1
0
mirror of https://github.com/Mailu/Mailu.git synced 2025-06-08 23:46:30 +02:00

Improve fetchmail

This commit is contained in:
Florent Daigniere 2022-11-13 17:15:50 +01:00
parent 8a90f83bd0
commit 08a9ab9a56
12 changed files with 88 additions and 8 deletions

View File

@ -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()

View File

@ -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)

View File

@ -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'))

View File

@ -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) }}

View File

@ -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>

View File

@ -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(

View File

@ -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')

View File

@ -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")

View File

@ -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 }}

View File

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

View File

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

View File

@ -0,0 +1 @@
Allow other folders to be synced by fetchmail