Improve fetchmail

main
Florent Daigniere 2 years ago
parent 8a90f83bd0
commit 08a9ab9a56

@ -12,10 +12,12 @@ def fetch_list():
"id": fetch.id, "id": fetch.id,
"tls": fetch.tls, "tls": fetch.tls,
"keep": fetch.keep, "keep": fetch.keep,
"scan": fetch.scan,
"user_email": fetch.user_email, "user_email": fetch.user_email,
"protocol": fetch.protocol, "protocol": fetch.protocol,
"host": fetch.host, "host": fetch.host,
"port": fetch.port, "port": fetch.port,
"folders": fetch.folders,
"username": fetch.username, "username": fetch.username,
"password": fetch.password "password": fetch.password
} for fetch in models.Fetch.query.all() } for fetch in models.Fetch.query.all()

@ -771,6 +771,8 @@ class Fetch(Base):
username = db.Column(db.String(255), nullable=False) username = db.Column(db.String(255), nullable=False)
password = db.Column(db.String(255), nullable=False) password = db.Column(db.String(255), nullable=False)
keep = db.Column(db.Boolean, nullable=False, default=False) keep = db.Column(db.Boolean, nullable=False, default=False)
scan = db.Column(db.Boolean, nullable=False, default=False)
folders = db.Column(CommaSeparatedList, nullable=True, default=list)
last_check = db.Column(db.DateTime, nullable=True) last_check = db.Column(db.DateTime, nullable=True)
error = db.Column(db.String(1023), nullable=True) error = db.Column(db.String(1023), nullable=True)

@ -41,6 +41,15 @@ class MultipleEmailAddressesVerify(object):
if not pattern.match(field.data.replace(" ", "")): if not pattern.match(field.data.replace(" ", "")):
raise validators.ValidationError(self.message) raise validators.ValidationError(self.message)
class MultipleFoldersVerify(object):
def __init__(self,message=_('Invalid list of folders.')):
self.message = message
def __call__(self, form, field):
pattern = re.compile(r'^\w+(,\w+)*$')
if not pattern.match(field.data.replace(" ", "")):
raise validators.ValidationError(self.message)
class ConfirmationForm(flask_wtf.FlaskForm): class ConfirmationForm(flask_wtf.FlaskForm):
submit = fields.SubmitField(_('Confirm')) submit = fields.SubmitField(_('Confirm'))
@ -164,11 +173,13 @@ class FetchForm(flask_wtf.FlaskForm):
('imap', 'IMAP'), ('pop3', 'POP3') ('imap', 'IMAP'), ('pop3', 'POP3')
]) ])
host = fields.StringField(_('Hostname or IP'), [validators.DataRequired()]) host = fields.StringField(_('Hostname or IP'), [validators.DataRequired()])
port = fields.IntegerField(_('TCP port'), [validators.DataRequired(), validators.NumberRange(min=0, max=65535)]) port = fields.IntegerField(_('TCP port'), [validators.DataRequired(), validators.NumberRange(min=0, max=65535)], default=993)
tls = fields.BooleanField(_('Enable TLS')) tls = fields.BooleanField(_('Enable TLS'), default=True)
username = fields.StringField(_('Username'), [validators.DataRequired()]) username = fields.StringField(_('Username'), [validators.DataRequired()])
password = fields.PasswordField(_('Password')) password = fields.PasswordField(_('Password'))
keep = fields.BooleanField(_('Keep emails on the server')) keep = fields.BooleanField(_('Keep emails on the server'))
scan = fields.BooleanField(_('Rescan emails locally'))
folders = fields.StringField(_('Folders to fetch on the server'), [validators.Optional(), MultipleFoldersVerify()], default='INBOX,Junk')
submit = fields.SubmitField(_('Submit')) submit = fields.SubmitField(_('Submit'))

@ -24,6 +24,8 @@
{%- call macros.card(title="Settings") %} {%- call macros.card(title="Settings") %}
{{ macros.form_field(form.keep) }} {{ macros.form_field(form.keep) }}
{{ macros.form_field(form.scan) }}
{{ macros.form_field(form.folders) }}
{%- endcall %} {%- endcall %}
{{ macros.form_field(form.submit) }} {{ macros.form_field(form.submit) }}

@ -20,6 +20,8 @@
<th>{% trans %}Endpoint{% endtrans %}</th> <th>{% trans %}Endpoint{% endtrans %}</th>
<th>{% trans %}Username{% endtrans %}</th> <th>{% trans %}Username{% endtrans %}</th>
<th>{% trans %}Keep emails{% endtrans %}</th> <th>{% trans %}Keep emails{% endtrans %}</th>
<th>{% trans %}Rescan emails{% endtrans %}</th>
<th>{% trans %}Folders{% endtrans %}</th>
<th>{% trans %}Last check{% endtrans %}</th> <th>{% trans %}Last check{% endtrans %}</th>
<th>{% trans %}Status{% endtrans %}</th> <th>{% trans %}Status{% endtrans %}</th>
<th>{% trans %}Created{% endtrans %}</th> <th>{% trans %}Created{% endtrans %}</th>
@ -36,6 +38,8 @@
<td>{{ fetch.protocol }}{{ 's' if fetch.tls else '' }}://{{ fetch.host }}:{{ fetch.port }}</td> <td>{{ fetch.protocol }}{{ 's' if fetch.tls else '' }}://{{ fetch.host }}:{{ fetch.port }}</td>
<td>{{ fetch.username }}</td> <td>{{ fetch.username }}</td>
<td data-sort="{{ fetch.keep }}">{% if fetch.keep %}{% trans %}yes{% endtrans %}{% else %}{% trans %}no{% endtrans %}{% endif %}</td> <td data-sort="{{ fetch.keep }}">{% if fetch.keep %}{% trans %}yes{% endtrans %}{% else %}{% trans %}no{% endtrans %}{% endif %}</td>
<td data-sort="{{ fetch.scan }}">{% if fetch.scan %}{% trans %}yes{% endtrans %}{% else %}{% trans %}no{% endtrans %}{% endif %}</td>
<td>{% for folder in fetch.folders %}{{ folder }},{% endfor %}</td>
<td>{{ fetch.last_check | format_datetime or '-' }}</td> <td>{{ fetch.last_check | format_datetime or '-' }}</td>
<td>{{ fetch.error or '-' }}</td> <td>{{ fetch.error or '-' }}</td>
<td data-sort="{{ fetch.created_at or '0000-00-00' }}">{{ fetch.created_at | format_date }}</td> <td data-sort="{{ fetch.created_at or '0000-00-00' }}">{{ fetch.created_at | format_date }}</td>

@ -26,6 +26,8 @@ def fetch_create(user_email):
if form.validate_on_submit(): if form.validate_on_submit():
fetch = models.Fetch(user=user) fetch = models.Fetch(user=user)
form.populate_obj(fetch) form.populate_obj(fetch)
if form.folders.data:
fetch.folders = form.folders.data.replace(' ','').split(',')
models.db.session.add(fetch) models.db.session.add(fetch)
models.db.session.commit() models.db.session.commit()
flask.flash('Fetch configuration created') flask.flash('Fetch configuration created')
@ -43,6 +45,8 @@ def fetch_edit(fetch_id):
if not form.password.data: if not form.password.data:
form.password.data = fetch.password form.password.data = fetch.password
form.populate_obj(fetch) form.populate_obj(fetch)
if form.folders.data:
fetch.folders = form.folders.data.replace(' ','').split(',')
models.db.session.commit() models.db.session.commit()
flask.flash('Fetch configuration updated') flask.flash('Fetch configuration updated')
return flask.redirect( return flask.redirect(

@ -0,0 +1,25 @@
"""empty message
Revision ID: f4f0f89e0047
Revises: 8f9ea78776f4
Create Date: 2022-11-13 16:29:01.246509
"""
# revision identifiers, used by Alembic.
revision = 'f4f0f89e0047'
down_revision = '8f9ea78776f4'
from alembic import op
import sqlalchemy as sa
import mailu
def upgrade():
with op.batch_alter_table('fetch') as batch:
batch.add_column(sa.Column('scan', sa.Boolean(), nullable=False, server_default=sa.sql.expression.false()))
batch.add_column(sa.Column('folders', mailu.models.CommaSeparatedList(), nullable=True))
def downgrade():
with op.batch_alter_table('fetch') as batch:
batch.drop_column('fetch', 'folders')
batch.drop_column('fetch', 'scan')

@ -2,11 +2,14 @@
import time import time
import os import os
from pathlib import Path
from pwd import getpwnam
import tempfile import tempfile
import shlex import shlex
import subprocess import subprocess
import re import re
import requests import requests
from socrate import system
import sys import sys
import traceback import traceback
@ -14,6 +17,7 @@ import traceback
FETCHMAIL = """ FETCHMAIL = """
fetchmail -N \ fetchmail -N \
--idfile /data/fetchids --uidl \ --idfile /data/fetchids --uidl \
--pidfile /dev/shm/fetchmail.pid \
--sslcertck --sslcertpath /etc/ssl/certs \ --sslcertck --sslcertpath /etc/ssl/certs \
-f {} -f {}
""" """
@ -24,7 +28,9 @@ poll "{host}" proto {protocol} port {port}
user "{username}" password "{password}" user "{username}" password "{password}"
is "{user_email}" is "{user_email}"
smtphost "{smtphost}" smtphost "{smtphost}"
{folders}
{options} {options}
{lmtp}
""" """
@ -48,26 +54,37 @@ def fetchmail(fetchmailrc):
def run(debug): def run(debug):
try: try:
fetches = requests.get("http://" + os.environ.get("HOST_ADMIN", "admin") + "/internal/fetch").json() os.environ["SMTP_ADDRESS"] = system.get_host_address_from_environment("SMTP", "smtp")
smtphost, smtpport = extract_host_port(os.environ.get("HOST_SMTP", "smtp"), None) os.environ["ADMIN_ADDRESS"] = system.get_host_address_from_environment("ADMIN", "admin")
fetches = requests.get(f"http://{os.environ['ADMIN_ADDRESS']}/internal/fetch").json()
smtphost, smtpport = extract_host_port(os.environ["SMTP_ADDRESS"], None)
if smtpport is None: if smtpport is None:
smtphostport = smtphost smtphostport = smtphost
else: else:
smtphostport = "%s/%d" % (smtphost, smtpport) smtphostport = "%s/%d" % (smtphost, smtpport)
os.environ["LMTP_ADDRESS"] = system.get_host_address_from_environment("LMTP", "imap:2525")
lmtphost, lmtpport = extract_host_port(os.environ["LMTP_ADDRESS"], None)
if lmtpport is None:
lmtphostport = lmtphost
else:
lmtphostport = "%s/%d" % (lmtphost, lmtpport)
for fetch in fetches: for fetch in fetches:
fetchmailrc = "" fetchmailrc = ""
options = "options antispam 501, 504, 550, 553, 554" options = "options antispam 501, 504, 550, 553, 554"
options += " ssl" if fetch["tls"] else "" options += " ssl" if fetch["tls"] else ""
options += " keep" if fetch["keep"] else " fetchall" options += " keep" if fetch["keep"] else " fetchall"
folders = "folders %s" % ((','.join('"' + item + '"' for item in fetch['folders'])) if fetch['folders'] else '"INBOX"')
fetchmailrc += RC_LINE.format( fetchmailrc += RC_LINE.format(
user_email=escape_rc_string(fetch["user_email"]), user_email=escape_rc_string(fetch["user_email"]),
protocol=fetch["protocol"], protocol=fetch["protocol"],
host=escape_rc_string(fetch["host"]), host=escape_rc_string(fetch["host"]),
port=fetch["port"], port=fetch["port"],
smtphost=smtphostport, smtphost=smtphostport if fetch['scan'] else lmtphostport,
username=escape_rc_string(fetch["username"]), username=escape_rc_string(fetch["username"]),
password=escape_rc_string(fetch["password"]), password=escape_rc_string(fetch["password"]),
options=options options=options,
folders=folders,
lmtp='' if fetch['scan'] else 'lmtp',
) )
if debug: if debug:
print(fetchmailrc) print(fetchmailrc)
@ -86,7 +103,7 @@ def run(debug):
user_info in error_message): user_info in error_message):
print(error_message) print(error_message)
finally: finally:
requests.post("http://" + os.environ.get("HOST_ADMIN", "admin") + "/internal/fetch/{}".format(fetch["id"]), requests.post("http://" + os.environ["ADMIN_ADDRESS"] + "/internal/fetch/{}".format(fetch["id"]),
json=error_message.split("\n")[0] json=error_message.split("\n")[0]
) )
except Exception: except Exception:
@ -94,6 +111,13 @@ def run(debug):
if __name__ == "__main__": if __name__ == "__main__":
id_fetchmail = getpwnam('fetchmail')
Path('/data/fetchids').touch()
os.chown("/data/fetchids", id_fetchmail.pw_uid, id_fetchmail.pw_gid)
os.chown("/data/", id_fetchmail.pw_uid, id_fetchmail.pw_gid)
os.chmod("/data/fetchids", 0o700)
os.setgid(id_fetchmail.pw_gid)
os.setuid(id_fetchmail.pw_uid)
while True: while True:
time.sleep(int(os.environ.get("FETCHMAIL_DELAY", 60))) time.sleep(int(os.environ.get("FETCHMAIL_DELAY", 60)))
run(os.environ.get("DEBUG", None) == "True") run(os.environ.get("DEBUG", None) == "True")

@ -157,8 +157,11 @@ services:
env_file: {{ env }} env_file: {{ env }}
volumes: volumes:
- "{{ root }}/data/fetchmail:/data" - "{{ root }}/data/fetchmail:/data"
{% if resolver_enabled %}
depends_on: depends_on:
- admin
- smtp
- imap
{% if resolver_enabled %}
- resolver - resolver
dns: dns:
- {{ dns }} - {{ dns }}

@ -0,0 +1 @@
Add an option so that emails fetched with fetchmail don't go through the filters (closes #1231)

@ -0,0 +1 @@
Fetchmail: Missing support for '*_ADDRESS' env vars

@ -0,0 +1 @@
Allow other folders to be synced by fetchmail
Loading…
Cancel
Save