From 30ecbf81cd0b80e35dde0aadd7125663f3df2cc9 Mon Sep 17 00:00:00 2001 From: Pierre Jaury Date: Thu, 28 Apr 2016 20:07:38 +0200 Subject: [PATCH] First fetchmail implementation --- admin/freeposte/admin/__init__.py | 3 +- admin/freeposte/admin/forms.py | 12 ++++ admin/freeposte/admin/models.py | 18 ++++- .../admin/templates/fetch/create.html | 9 +++ .../freeposte/admin/templates/fetch/edit.html | 9 +++ .../freeposte/admin/templates/fetch/list.html | 44 ++++++++++++ admin/freeposte/admin/templates/sidebar.html | 2 +- .../admin/templates/user/fetchmail.html | 1 - admin/freeposte/admin/utils.py | 10 +++ admin/freeposte/admin/views/fetches.py | 68 +++++++++++++++++++ admin/freeposte/admin/views/users.py | 7 -- docker-compose.yml | 7 ++ fetchmail/Dockerfile | 13 ++++ fetchmail/fetchmail.py | 54 +++++++++++++++ 14 files changed, 246 insertions(+), 11 deletions(-) create mode 100644 admin/freeposte/admin/templates/fetch/create.html create mode 100644 admin/freeposte/admin/templates/fetch/edit.html create mode 100644 admin/freeposte/admin/templates/fetch/list.html delete mode 100644 admin/freeposte/admin/templates/user/fetchmail.html create mode 100644 admin/freeposte/admin/views/fetches.py create mode 100644 fetchmail/Dockerfile create mode 100755 fetchmail/fetchmail.py diff --git a/admin/freeposte/admin/__init__.py b/admin/freeposte/admin/__init__.py index 179d5155..6b2639c8 100644 --- a/admin/freeposte/admin/__init__.py +++ b/admin/freeposte/admin/__init__.py @@ -26,4 +26,5 @@ from freeposte.admin.views import \ base, \ aliases, \ users, \ - domains + domains, \ + fetches diff --git a/admin/freeposte/admin/forms.py b/admin/freeposte/admin/forms.py index dbd1eab9..d208abf4 100644 --- a/admin/freeposte/admin/forms.py +++ b/admin/freeposte/admin/forms.py @@ -71,3 +71,15 @@ class AdminForm(Form): class ManagerForm(Form): manager = fields.StringField('Manager address', [validators.Email()]) submit = fields.SubmitField('Submit') + + +class FetchForm(Form): + protocol = fields.SelectField('Protocol', choices=[ + ('imap', 'IMAP'), ('pop3', 'POP3') + ]) + host = fields.StringField('Hostname or IP') + port = fields.IntegerField('TCP port') + tls = fields.BooleanField('Enable TLS') + username = fields.StringField('Username') + password = fields.StringField('Password') + submit = fields.SubmitField('Submit') diff --git a/admin/freeposte/admin/models.py b/admin/freeposte/admin/models.py index 7e16c9f9..fea5ff39 100644 --- a/admin/freeposte/admin/models.py +++ b/admin/freeposte/admin/models.py @@ -10,7 +10,7 @@ import re # Many-to-many association table for domain managers managers = db.Table('manager', db.Column('domain_name', db.String(80), db.ForeignKey('domain.name')), - db.Column('user_address', db.String(80), db.ForeignKey('user.address')) + db.Column('user_address', db.String(255), db.ForeignKey('user.address')) ) @@ -143,3 +143,19 @@ class Alias(Address): """ domain = db.relationship(Domain, backref='aliases') destination = db.Column(db.String(), nullable=False) + + +class Fetch(Base): + """ A fetched account is a repote POP/IMAP account fetched into a local + account. + """ + id = db.Column(db.Integer(), primary_key=True) + user_address = db.Column(db.String(255), db.ForeignKey(User.address), + nullable=False) + user = db.relationship(User, backref='fetches') + protocol = db.Column(db.Enum('imap', 'pop3'), nullable=False) + host = db.Column(db.String(255), nullable=False) + port = db.Column(db.Integer(), nullable=False) + tls = db.Column(db.Boolean(), nullable=False) + username = db.Column(db.String(255), nullable=False) + password = db.Column(db.String(255), nullable=False) diff --git a/admin/freeposte/admin/templates/fetch/create.html b/admin/freeposte/admin/templates/fetch/create.html new file mode 100644 index 00000000..f0b470e3 --- /dev/null +++ b/admin/freeposte/admin/templates/fetch/create.html @@ -0,0 +1,9 @@ +{% extends "form.html" %} + +{% block title %} +Add a fetched account +{% endblock %} + +{% block subtitle %} +{{ user }} +{% endblock %} diff --git a/admin/freeposte/admin/templates/fetch/edit.html b/admin/freeposte/admin/templates/fetch/edit.html new file mode 100644 index 00000000..bac05427 --- /dev/null +++ b/admin/freeposte/admin/templates/fetch/edit.html @@ -0,0 +1,9 @@ +{% extends "form.html" %} + +{% block title %} +Update a fetched account +{% endblock %} + +{% block subtitle %} +{{ user }} +{% endblock %} diff --git a/admin/freeposte/admin/templates/fetch/list.html b/admin/freeposte/admin/templates/fetch/list.html new file mode 100644 index 00000000..f871d61a --- /dev/null +++ b/admin/freeposte/admin/templates/fetch/list.html @@ -0,0 +1,44 @@ +{% extends "base.html" %} + +{% block title %} +Fetched accounts +{% endblock %} + +{% block subtitle %} +{{ user }} +{% endblock %} + +{% block main_action %} +Add an account +{% endblock %} + +{% block box %} + + + + + + + + + + + + + {% for fetch in user.fetches %} + + + + + + + + + + {% endfor %} + +
ActionsProtocolHostnamePortTLSUsernameCreatedLast edit
+   + + {{ fetch.protocol }}{{ fetch.host }}{{ fetch.port }}{{ fetch.tls }}{{ fetch.created_at }}{{ fetch.updated_at or '' }}
+{% endblock %} diff --git a/admin/freeposte/admin/templates/sidebar.html b/admin/freeposte/admin/templates/sidebar.html index 78b194b0..2886140a 100644 --- a/admin/freeposte/admin/templates/sidebar.html +++ b/admin/freeposte/admin/templates/sidebar.html @@ -24,7 +24,7 @@
  • - + Fetched accounts
  • diff --git a/admin/freeposte/admin/templates/user/fetchmail.html b/admin/freeposte/admin/templates/user/fetchmail.html deleted file mode 100644 index a500fb36..00000000 --- a/admin/freeposte/admin/templates/user/fetchmail.html +++ /dev/null @@ -1 +0,0 @@ -{% extends "working.html" %} diff --git a/admin/freeposte/admin/utils.py b/admin/freeposte/admin/utils.py index b6542501..ff2c8bf2 100644 --- a/admin/freeposte/admin/utils.py +++ b/admin/freeposte/admin/utils.py @@ -40,3 +40,13 @@ def get_alias(alias): if not alias.domain in flask_login.current_user.get_managed_domains(): return 403 return alias + + +def get_fetch(fetch_id): + fetch = models.Fetch.query.filter_by(id=fetch_id).first() + if not fetch: + flask.abort(404) + if not fetch.user.domain in flask_login.current_user.get_managed_domains(): + if not fetch.user == flask_login.current_user: + flask.abort(403) + return fetch diff --git a/admin/freeposte/admin/views/fetches.py b/admin/freeposte/admin/views/fetches.py new file mode 100644 index 00000000..6a16406e --- /dev/null +++ b/admin/freeposte/admin/views/fetches.py @@ -0,0 +1,68 @@ +from freeposte.admin import app, db, models, forms, utils +from flask.ext import login as flask_login + +import os +import flask +import wtforms_components + + +@app.route('/fetch/list', methods=['GET', 'POST'], defaults={'user_address': None}) +@app.route('/fetch/list/', methods=['GET']) +@flask_login.login_required +def fetch_list(user_address): + user = utils.get_user(user_address, True) + return flask.render_template('fetch/list.html', user=user) + + +@app.route('/fetch/list', methods=['GET', 'POST'], defaults={'user_address': None}) +@app.route('/fetch/create/', methods=['GET', 'POST']) +@flask_login.login_required +def fetch_create(user_address): + user = utils.get_user(user_address) + form = forms.FetchForm() + if form.validate_on_submit(): + fetch = models.Fetch(user=user) + fetch.protocol = form.protocol.data + fetch.host = form.host.data + fetch.port = form.port.data + fetch.tls = form.tls.data + fetch.username = form.username.data + fetch.password = form.password.data + db.session.add(fetch) + db.session.commit() + flask.flash('Fetch configuration created') + return flask.redirect( + flask.url_for('.fetch_create', user_address=user.address)) + return flask.render_template('fetch/create.html', form=form) + + +@app.route('/fetch/edit/', methods=['GET', 'POST']) +@flask_login.login_required +def fetch_edit(fetch_id): + fetch = utils.get_fetch(fetch_id) + form = forms.FetchForm(obj=fetch) + if form.validate_on_submit(): + fetch.protocol = form.protocol.data + fetch.host = form.host.data + fetch.port = form.port.data + fetch.tls = form.tls.data + fetch.username = form.username.data + fetch.password = form.password.data + db.session.add(fetch) + db.session.commit() + flask.flash('Fetch configuration updated') + return flask.redirect( + flask.url_for('.fetch_list', user_address=fetch.user.address)) + return flask.render_template('fetch/edit.html', + form=form, fetch=fetch) + + +@app.route('/fetch/delete/', methods=['GET']) +@flask_login.login_required +def fetch_delete(fetch_id): + fetch = utils.get_fetch(fetch_id) + db.session.delete(fetch) + db.session.commit() + flask.flash('Fetch configuration delete') + return flask.redirect( + flask.url_for('.fetch_list', user_address=fetch.user.address)) diff --git a/admin/freeposte/admin/views/users.py b/admin/freeposte/admin/views/users.py index 2511e9a2..b1c0d298 100644 --- a/admin/freeposte/admin/views/users.py +++ b/admin/freeposte/admin/views/users.py @@ -149,10 +149,3 @@ def user_reply(user_email): return flask.redirect( flask.url_for('.user_list', domain_name=user.domain.name)) return flask.render_template('user/reply.html', form=form, user=user) - - -@app.route('/user/fetchmail', methods=['GET', 'POST'], defaults={'user_email': None}) -@app.route('/user/fetchmail/', methods=['GET', 'POST']) -@flask_login.login_required -def user_fetchmail(user_email): - return flask.render_template('user/fetchmail.html') diff --git a/docker-compose.yml b/docker-compose.yml index 5fc2dfdd..22af51a1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -60,3 +60,10 @@ services: env_file: freeposte.env volumes: - /data/webmail:/data + + fetchmail: + build: fetchmail + image: freeposte/fetchmail + env_file: freeposte.env + volumes: + - /data/freeposte:/data diff --git a/fetchmail/Dockerfile b/fetchmail/Dockerfile new file mode 100644 index 00000000..4a1931f0 --- /dev/null +++ b/fetchmail/Dockerfile @@ -0,0 +1,13 @@ +FROM python:alpine + +RUN apk add --update \ + fetchmail \ + && rm -rf /var/cache/apk/* + +COPY fetchmail.py /fetchmail.py + +RUN mkdir /var/spool/mail \ + && chown mail: /var/spool/mail +USER mail + +CMD ["/fetchmail.py"] diff --git a/fetchmail/fetchmail.py b/fetchmail/fetchmail.py new file mode 100755 index 00000000..c90a9817 --- /dev/null +++ b/fetchmail/fetchmail.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python + +import sqlite3 +import time +import os +import tempfile + + +RC_LINE = """ +poll {host} proto {protocol} port {port} + user "{username}" password "{password}" + smtphost "smtp" + smtpname {user_address} + {options} +""" + + +def fetchmail(fetchmailrc): + print(fetchmailrc) + with tempfile.NamedTemporaryFile() as handler: + handler.write(fetchmailrc.encode("utf8")) + handler.flush() + os.system("fetchmail -N -f '{}'".format(handler.name)) + + +def run(cursor): + cursor.execute(""" + SELECT user_address, protocol, host, port, tls, username, password + FROM fetch + """) + fetchmailrc = "" + for line in cursor.fetchall(): + user_address, protocol, host, port, tls, username, password = line + options = "options ssl" if tls else "" + fetchmailrc += RC_LINE.format( + user_address=user_address, + protocol=protocol, + host=host, + port=port, + username=username, + password=password, + options=options + ) + fetchmail(fetchmailrc) + + +if __name__ == "__main__": + db_path = os.environ.get("DB_PATH", "/data/freeposte.db") + connection = sqlite3.connect(db_path) + while True: + time.sleep(int(os.environ.get("FETCHMAIL_DELAY", 10))) + cursor = connection.cursor() + run(cursor) + cursor.close()