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 %}
+
+
+
+ Actions |
+ Protocol |
+ Hostname |
+ Port |
+ TLS |
+ Username |
+ Created |
+ Last edit |
+
+ {% for fetch in user.fetches %}
+
+
+
+
+ |
+ {{ fetch.protocol }} |
+ {{ fetch.host }} |
+ {{ fetch.port }} |
+ {{ fetch.tls }} |
+ {{ fetch.created_at }} |
+ {{ fetch.updated_at or '' }} |
+
+ {% endfor %}
+
+
+{% 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()