summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--all.yml1
-rw-r--r--common.yml2
-rw-r--r--group_vars/all.yml2
-rw-r--r--lists.yml5
-rw-r--r--roles/common-LDAP/templates/etc/ldap/database.ldif.j22
-rw-r--r--roles/common/files/etc/postfix/master.cf7
-rw-r--r--roles/lists/files/etc/cron.d/mlmmj1
-rw-r--r--roles/lists/files/etc/mhonarc.rc421
l---------roles/lists/files/etc/postfix/virtual/mailbox_domains.cf1
-rw-r--r--roles/lists/files/etc/postfix/virtual/transport_lists_maps.cf7
-rwxr-xr-xroles/lists/files/usr/local/bin/mhonarc-scan.sh114
-rwxr-xr-xroles/lists/files/usr/local/bin/mlmmj-newlist.sh139
-rw-r--r--roles/lists/files/var/lib/mlmmj/static/css/fripost.css63
-rw-r--r--roles/lists/handlers/main.yml6
-rw-r--r--roles/lists/tasks/mail.yml35
-rw-r--r--roles/lists/tasks/main.yml2
-rw-r--r--roles/lists/tasks/mlmmj.yml78
-rw-r--r--roles/lists/templates/etc/postfix/main.cf.j276
18 files changed, 960 insertions, 2 deletions
diff --git a/all.yml b/all.yml
index b4ea79d..6a62511 100644
--- a/all.yml
+++ b/all.yml
@@ -7,3 +7,4 @@
- include: MX.yml
- include: MSA.yml
- include: webmail.yml
+- include: lists.yml
diff --git a/common.yml b/common.yml
index 38995df..99628a3 100644
--- a/common.yml
+++ b/common.yml
@@ -34,7 +34,7 @@
- LDAP-provider
- name: Configure the Web servers
- hosts: webmail
+ hosts: webmail:lists
gather_facts: False
tags: nginx,www,web
roles:
diff --git a/group_vars/all.yml b/group_vars/all.yml
index 7bd5fe1..0dee19d 100644
--- a/group_vars/all.yml
+++ b/group_vars/all.yml
@@ -8,9 +8,11 @@ postfix_instance:
MTA-out: { name: mta-out,group: mta }
MSA: { name: msa }
webmail: { name: webmail }
+ lists: { name: lists }
MTA_out: { host: outgoing.fripost.org, port: 2525 }
LDA: { host: lda.fripost.org, port: 2526 }
+lists: { host: lists.fripost.org, port: 2527 }
LDAP_provider: host1.libvirt.guilhem.org
NTP_master: host1.libvirt.guilhem.org
diff --git a/lists.yml b/lists.yml
new file mode 100644
index 0000000..1478ed4
--- /dev/null
+++ b/lists.yml
@@ -0,0 +1,5 @@
+---
+- name: Configure the list managers
+ hosts: lists
+ roles:
+ - { role: lists, inst: lists }
diff --git a/roles/common-LDAP/templates/etc/ldap/database.ldif.j2 b/roles/common-LDAP/templates/etc/ldap/database.ldif.j2
index b4c2c4f..6e5961b 100644
--- a/roles/common-LDAP/templates/etc/ldap/database.ldif.j2
+++ b/roles/common-LDAP/templates/etc/ldap/database.ldif.j2
@@ -97,6 +97,8 @@ olcSyncrepl: rid=000
binddn="cn=MX-replicate,ou=services,o=mailHosting,dc=fripost,dc=org"
credentials=mx
{% elif 'lists' in group_names %}
+# XXX: mlmmj is not compatible with the MX, see
+# http://mlmmj.org/bugs/bug.php?id=51
olcSyncrepl: rid=001
provider=ldap://{{ LDAP_provider }}
type=refreshAndPersist
diff --git a/roles/common/files/etc/postfix/master.cf b/roles/common/files/etc/postfix/master.cf
index 325af1b..3833446 100644
--- a/roles/common/files/etc/postfix/master.cf
+++ b/roles/common/files/etc/postfix/master.cf
@@ -40,14 +40,19 @@ anvil unix - - - - 1 anvil
scache unix - - - - 1 scache
127.0.0.1:16132 inet n - - - - smtpd
127.0.0.1:2526 inet n - - - - smtpd
-127.0.0.1:2527 inet n - - - - smtpd
+2527 inet n - - - - smtpd
+ -o mynetworks=0.0.0.0/0
127.0.0.1:2580 inet n - - - - smtpd
127.0.0.1:2599 inet n - - - - smtpd
-o cleanup_service_name=cleanup-catchall
cleanup-catchall unix n - - - 0 cleanup
-o virtual_alias_maps=cdb:$config_directory/virtual/reserved_alias_maps,ldap:$config_directory/virtual/alias_maps.cf,ldap:/etc/postfix-mx/virtual/catchall_maps.cf
+127.0.0.1:smtp inet n - - - - smtpd
+ -o inet_interfaces=127.0.0.1
reserved-alias unix - n n - - pipe
flags=Rhu user=nobody argv=/usr/local/sbin/reserved-alias.pl ${sender} ${original_recipient} @fripost.org
+mlmmj unix - n n - - pipe
+ flags=Rhu user=mlmmj argv=/usr/bin/mlmmj-receive -L /var/spool/mlmmj/${domain}/${user}
amavisfeed unix - - n - 2 lmtp
-o lmtp_destination_recipient_limit=1000
-o lmtp_send_xforward_command=yes
diff --git a/roles/lists/files/etc/cron.d/mlmmj b/roles/lists/files/etc/cron.d/mlmmj
new file mode 100644
index 0000000..2f34265
--- /dev/null
+++ b/roles/lists/files/etc/cron.d/mlmmj
@@ -0,0 +1 @@
+0 */2 * * * mlmmj /usr/bin/test -x /usr/bin/mlmmj-maintd && /usr/bin/mlmmj-maintd -F -d /var/spool/mlmmj
diff --git a/roles/lists/files/etc/mhonarc.rc b/roles/lists/files/etc/mhonarc.rc
new file mode 100644
index 0000000..bcb967e
--- /dev/null
+++ b/roles/lists/files/etc/mhonarc.rc
@@ -0,0 +1,421 @@
+<Include>
+/usr/share/doc/mhonarc/examples/utf-8-encode.mrc
+/usr/share/doc/mhonarc/examples/secure.mrc
+/usr/share/doc/mhonarc/examples/def-mime.mrc.gz
+</Include>
+
+<NoPrintXComments>
+
+<FieldOrder>
+from
+to
+subject
+date
+</FieldOrder>
+
+<LabelStyles>
+-default-
+</LabelStyles>
+
+<MIMEArgs>
+text/plain; fancyquote
+text/html; disableflowed
+text/x-html; disableflowed
+</MIMEArgs>
+
+<MIMEIncs>
+text
+image
+message
+application/pgp-encrypted
+application/pgp-signature
+</MIMEIncs>
+
+<NOFOLREFS>
+<TSlice>
+3:7:1
+</TSlice>
+
+<POSIXSTRFTIME>
+<LocalDateFmt>
+%a %b %d %T %Z %Y
+</LocalDateFmt>
+
+<ModifybodyAddresses>
+
+<AddressModifyCode>
+s#\@(\w+)([.-]\w+)#'@'.('x' x length($1)).$2#e;
+</AddressModifyCode>
+
+<MailToURL>
+mailto:$TOADDRNAME$.AT.$TOADDRDOMAIN$
+</MailToURL>
+
+
+
+<!-- Have attachments written to a sub-directory -->
+<AttachmentDir>
+attachments
+</AttachmentDir>
+
+<!-- Define base URL to attachment directory. -->
+<AttachmentURL>
+attachments
+</AttachmentURL>
+
+<CheckNoArchive>
+
+<DEFINEVAR>
+ListHeader
+ <p class="muted credit pull-right">
+ <a title="$ListName$'s home page" href="$ListPage$">$ListName$</a>'s archives
+ $DirDate$
+ ($NUMOFMSG$ messages)
+ </p>
+</DEFINEVAR>
+
+<!-- Messages -->
+
+<MsgPgBegin>
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>$SUBJECTNA$</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta name="description" content="">
+ <meta name="author" content="">
+
+ <link href="/static/css/bootstrap.min.css" rel="stylesheet">
+ <link href="/static/css/fripost.css" rel="stylesheet">
+ </head>
+
+ <body>
+</MsgPgBegin>
+
+<MsgPgEnd>
+ </div>
+ </div>
+ <div id="footer">
+ $MsgNav$
+ <p class="muted credit pull-right">
+ Last update by <a href="http://mhonarc.org">MHonArc</a> on $LOCALDATE$.
+ </p>
+ </div>
+ </body>
+</html>
+</MsgPgEnd>
+
+
+<PrevButton chop>
+<a title="Date prev" href="$MSG(PREV)$" class="glyphicon glyphicon-backward"></a>
+</PrevButton>
+<PrevButtonIA chop>
+<span class="glyphicon glyphicon-backward"></span>
+</PrevButtonIA>
+
+<NextButton chop>
+<a title="Date next" href="$MSG(NEXT)$" class="glyphicon glyphicon-forward"></a>
+</NextButton>
+<NextButtonIA chop>
+<span class="glyphicon glyphicon-forward"></span>
+</NextButtonIA>
+
+<TPrevInButton chop>
+<a title="Thread prev" href="$MSG(TPREVIN)$" class="glyphicon glyphicon-backward"></a>
+</TPrevInButton>
+<TPrevInButtonIA chop>
+<span class="glyphicon glyphicon-backward"></span>
+</TPrevInButtonIA>
+
+<TNextInButton chop>
+<a title="Thread next" href="$MSG(TNEXTIN)$" class="glyphicon glyphicon-forward"></a>
+</TNextInButton>
+<TNextInButtonIA chop>
+<span class="glyphicon glyphicon-forward"></span>
+</TNextInButtonIA>
+
+<DEFINEVAR>
+MsgNav
+$BUTTON(PREV)$ <a title="Date index" href="$IDXFNAME$#$MSGNUM$">Date</a> $BUTTON(NEXT)$&emsp;$BUTTON(TPREVIN)$ <a title="Thread index" href="$TIDXFNAME$#$MSGNUM$">Thread</a> $BUTTON(TNEXTIN)$
+</DEFINEVAR>
+
+<TSubjectBeg>
+<li class="dummy small">(possible follow-ups)</li>
+</TSubjectBeg>
+
+<TContBegin>
+<li><strong>$SUBJECTNA$</strong> <span class="dummy small">(continued)</span></li>
+</TContBegin>
+
+<TopLinks>
+ <div id="header">
+ $MsgNav$
+ $ListHeader$
+ </div>
+ <div id="wrap">
+ <div id="main" class="container">
+</TopLinks>
+
+<SubjectHeader>
+<h1 class="msg subject">$SUBJECTNA$</h1>
+</SubjectHeader>
+
+<FieldsBeg>
+<div class="well msg header">
+ <table>
+</FieldsBeg>
+
+<LabelBeg>
+<tr>
+ <td>
+</LabelBeg>
+
+<LabelEnd>
+ </td>
+</LabelEnd>
+
+<FldBeg>
+ <td>
+</FldBeg>
+
+<FldEnd>
+ </td>
+</tr>
+</FldEnd>
+
+<FieldsEnd>
+ </table>
+</div>
+</FieldsEnd>
+
+
+<HeadBodySep>
+<div class="msg body">
+</HeadBodySep>
+
+<MsgBodyEnd>
+</div>
+<div class="well msg footer">
+</MsgBodyEnd>
+
+<FolUpBegin>
+<strong>Follow-Ups ($NUMFOLUP$):</strong>
+<ul>
+</FolUpBegin>
+
+<FolupLiTXT>
+<li><strong>$SUBJECT$</strong>, <em>$FROMNAME$</em></li>
+</FolupLiTXT>
+
+<FolUpEnd>
+</ul>
+<hr>
+</FolUpEnd>
+
+<TSliceBeg>
+<strong>References:</strong>
+<ul>
+</TSliceBeg>
+
+<TSliceTopBeginCur>
+<li><strong>$SUBJECTNA$</strong>, <em>$FROMNAME$</em>
+</TSliceTopBeginCur>
+
+<TSliceLiTxtCur>
+<li><strong>$SUBJECTNA$</strong>, <em>$FROMNAME$</em>
+</TSliceLiTxtCur>
+
+<TSliceSingleTxtCur>
+<li><strong>$SUBJECTNA$</strong>, <em>$FROMNAME$</em>
+</TSliceSingleTxtCur>
+
+<TSliceEnd>
+</ul>
+</TSliceEnd>
+
+<RefsBegin>
+<strong>References:</strong>
+<ul>
+</RefsBegin>
+
+<RefsLiTxt>
+<li><strong>$SUBJECT$</strong>, <em>$FROMNAME$</em></li>
+</RefsLiTxt>
+
+<RefsEnd>
+</ul>
+<hr>
+</RefsEnd>
+
+<BotLinks>
+$TSLICE$
+</div>
+</BotLinks>
+
+
+
+<!-- Thread index -->
+
+<TLEVELS>
+5
+</TLEVELS>
+
+<MULTIPG>
+<IDXSIZE>
+250
+</IDXSIZE>
+
+<TFirstPgLink chop>
+<a title="First page" href="$PG(TFIRST)$" class="glyphicon glyphicon-fast-backward"></a>
+</TFirstPgLink>
+
+<TPrevPgLink chop>
+<a title="Previous page" href="$PG(TPREV)$" class="glyphicon glyphicon-backward"></a>
+</TPrevPgLink>
+<TPrevPgLinkIA chop>
+<span class="glyphicon glyphicon-backward"></span>
+</TPrevPgLinkIA>
+
+<TNextPgLink chop>
+<a title="Next page" href="$PG(TNEXT)$" class="glyphicon glyphicon-forward"></a>
+</TNextPgLink>
+<TNextPgLinkIA chop>
+<span class="glyphicon glyphicon-forward"></span>
+</TNextPgLinkIA>
+
+<TLastPgLink chop>
+<a title="Last page" href="$PG(TLAST)$" class="glyphicon glyphicon-fast-forward"></a>
+</TLastPgLink>
+
+<DEFINEVAR>
+TNav
+$PGLINK(TFIRST)$$PGLINK(TPREV)$ Page $PAGENUM$/$NUMOFPAGES$ $PGLINK(TNEXT)$$PGLINK(TLAST)$
+[Indexes: <a href="$IDXFNAME$">Date</a> Thread]
+</DEFINEVAR>
+
+<THead>
+ <div id="wrap">
+ <div id="main" class="container">
+ <ul class="index">
+</THead>
+<TFoot>
+ </ul>
+ </div>
+ </div>
+</TFoot>
+
+<TIdxPgBegin>
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>$TIDXTITLE$</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta name="description" content="">
+ <meta name="author" content="">
+
+ <link href="/static/css/bootstrap.min.css" rel="stylesheet">
+ <link href="/static/css/fripost.css" rel="stylesheet">
+ </head>
+
+ <body>
+ <div id="header">
+ $TNav$
+ $ListHeader$
+ </div>
+</TIdxPgBegin>
+
+<NoDoc>
+<TIdxPgEnd>
+ <div id="footer">
+ $TNav$
+ <p class="muted credit pull-right">
+ Page generated by <a href="http://mhonarc.org">MHonArc</a> on $LOCALDATE$.
+ </p>
+ </div>
+ </body>
+</html>
+</TIdxPgEnd>
+
+<LiTemplate>
+<li><strong>$SUBJECT$</strong>,
+<em>$FROMNAME$</em>
+</li>
+</LiTemplate>
+
+
+
+
+<!-- Date index -->
+
+<FirstPgLink chop>
+<a title="First page" href="$PG(FIRST)$" class="glyphicon glyphicon-fast-backward"></a>
+</FirstPgLink>
+
+<PrevPgLink chop>
+<a title="Previous page" href="$PG(PREV)$" class="glyphicon glyphicon-backward"></a>
+</PrevPgLink>
+<PrevPgLinkIA chop>
+<span class="glyphicon glyphicon-backward"></span>
+</PrevPgLinkIA>
+
+<NextPgLink chop>
+<a title="Next page" href="$PG(NEXT)$" class="glyphicon glyphicon-forward"></a>
+</NextPgLink>
+<NextPgLinkIA chop>
+<span class="glyphicon glyphicon-forward"></span>
+</NextPgLinkIA>
+
+<LastPgLink chop>
+<a title="Last page" href="$PG(LAST)$" class="glyphicon glyphicon-fast-forward"></a>
+</LastPgLink>
+
+<DEFINEVAR>
+Nav
+$PGLINK(FIRST)$$PGLINK(PREV)$ Page $PAGENUM$/$NUMOFPAGES$ $PGLINK(NEXT)$$PGLINK(LAST)$
+[Indexes: Date <a href="$TIDXFNAME$">Thread</a>]
+</DEFINEVAR>
+
+<ListBegin>
+ <div id="wrap">
+ <div id="main" class="container">
+ <ul class="index">
+</ListBegin>
+<ListEnd>
+ </ul>
+ </div>
+ </div>
+</ListEnd>
+
+<IdxPgBegin>
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>$IDXTITLE$</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta name="description" content="">
+ <meta name="author" content="">
+
+ <link href="/static/css/bootstrap.min.css" rel="stylesheet">
+ <link href="/static/css/fripost.css" rel="stylesheet">
+ </head>
+
+ <body>
+ <div id="header">
+ $Nav$
+ $ListHeader$
+ </div>
+</IdxPgBegin>
+
+<IdxPgEnd>
+ <div id="footer">
+ $Nav$
+ <p class="muted credit pull-right">
+ Last update by <a href="http://mhonarc.org">MHonArc</a> on $LOCALDATE$.
+ </p>
+ </div>
+ </body>
+</html>
+</IdxPgEnd>
diff --git a/roles/lists/files/etc/postfix/virtual/mailbox_domains.cf b/roles/lists/files/etc/postfix/virtual/mailbox_domains.cf
new file mode 120000
index 0000000..05f7ed9
--- /dev/null
+++ b/roles/lists/files/etc/postfix/virtual/mailbox_domains.cf
@@ -0,0 +1 @@
+../../../../../MX/templates/etc/postfix/virtual/mailbox_domains.cf.j2 \ No newline at end of file
diff --git a/roles/lists/files/etc/postfix/virtual/transport_lists_maps.cf b/roles/lists/files/etc/postfix/virtual/transport_lists_maps.cf
new file mode 100644
index 0000000..50631e5
--- /dev/null
+++ b/roles/lists/files/etc/postfix/virtual/transport_lists_maps.cf
@@ -0,0 +1,7 @@
+server_host = ldapi://%2Fprivate%2Fldapi/
+version = 3
+search_base = fvl=%u,fvd=%d,ou=virtual,o=mailHosting,dc=fripost,dc=org
+scope = base
+bind = none
+query_filter = (&(objectClass=FripostVirtualList)(fvl=%u))
+result_attribute = fripostListManager
diff --git a/roles/lists/files/usr/local/bin/mhonarc-scan.sh b/roles/lists/files/usr/local/bin/mhonarc-scan.sh
new file mode 100755
index 0000000..d0ea2af
--- /dev/null
+++ b/roles/lists/files/usr/local/bin/mhonarc-scan.sh
@@ -0,0 +1,114 @@
+#!/bin/sh
+
+# Convert a list archive into HTML.
+#
+# Copyright © 2014 Guilhem Moulin <guilhem@fripost.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+set -ue
+
+fail () {
+ echo Error: "$@" >&2
+ exit 1
+}
+
+[ $# -eq 1 ] || { echo "Usage: $0 listdir"; exit; }
+listdir="${1%/}"
+[ -d "$listdir" ] || fail "No such directory: $listdir"
+
+localpart="${listdir##*/}"
+domainpart="${listdir%/$localpart}"
+domainpart="${domainpart##*/}"
+
+# Determine the rotation period
+rotation=
+[ -s "$listdir/control/archiverotate" ] && read rotation <"$listdir/control/archiverotate"
+
+# Subdir format (/!\ shouldn't be empty, and shouldn't contain spaces!),
+# and archive date format.
+case "${rotation:-month}" in
+ '') subdirf='/' listpage='./'; archivef=;;
+ year) subdirf="%Y"; listpage='../'; archivef="for %Y";;
+ month) subdirf="%Y/%m"; listpage='../../'; archivef="for %B %Y";;
+ day) subdirf="%Y/%m/%d"; listpage='../../../'; archivef="for %a, %d %b %Y";;
+ *) fail "$rotation: unknown rotation period"
+esac
+
+# Look up for the send date in an email. Fall back to the creation date
+# if not found.
+printDate () {
+ local filename date
+ while read filename; do
+ if ! [ "$rotation" ]; then
+ # don't bother looking for a date
+ date=0
+ else
+ # stop as soon as the header is over
+ date=$(sed -nr '/^Date:\s*(\S.*)$/I {s//\1/p;q}; /^([^[:cntrl:][:space:]]+:|\s)/ !q' \
+ "$filename")
+ [ "$date" ] || date=@$(stat -c '%Y' "$filename")
+ fi
+ echo $(date -d "$date" +"%s $subdirf") "$filename"
+ done
+}
+
+# Process a (single) subdirectory
+process () {
+ local list="$1" subdir="$2" date="$3"
+ [ -s "$list" ] || return 0
+
+ [ -d "$listdir/webarchive/$subdir" ] || mkdir -p "$listdir/webarchive/$subdir"
+ # TODO: add a line to the index file
+ xargs -a"$list" mhonarc -definevar ListName="'$localpart'" \
+ -definevar ListPage="'${listpage}index.html'" \
+ -definevar DirDate="'$date'" \
+ -rcfile /etc/mhonarc.rc \
+ -add \
+ -quiet \
+ -outdir "$listdir/webarchive/$subdir" \
+ || exit 1
+ # empty the list
+ echo -n >"$list"
+}
+
+# Process all found emails
+processM () {
+ local cursubdir= date=
+ local timestamp subdir filename
+
+ while read timestamp subdir filename; do
+ if [ "$cursubdir" != "$subdir" ]; then
+ process "$list" "$cursubdir" "$date"
+ cursubdir="$subdir"
+ date="$(date -d "@$timestamp" +"$archivef")"
+ fi
+ echo "$filename" >>"$list"
+ done
+ process "$list" "$cursubdir" "$date"
+}
+
+# The span of emails we'll touch during the current instance
+now=$(date +'%s')
+list=$(mktemp) || exit 1
+trap 'rm -f "$list"' EXIT
+
+from=0
+if [ -s "$listdir/.webarchive.date" ]; then
+ read from <"$listdir/.webarchive.date"
+ from=$(( $from - 30 )) # remove 30s to fight race conditions
+fi
+
+find "$listdir/archive/" -type f -a -newermt @"$from" | printDate | sort -n -k1,1 | processM
+echo "$now" > "$listdir/.webarchive.date"
diff --git a/roles/lists/files/usr/local/bin/mlmmj-newlist.sh b/roles/lists/files/usr/local/bin/mlmmj-newlist.sh
new file mode 100755
index 0000000..a1fcc70
--- /dev/null
+++ b/roles/lists/files/usr/local/bin/mlmmj-newlist.sh
@@ -0,0 +1,139 @@
+#!/bin/sh
+
+# Add new lists (with a common options) to be managed by mlmmj.
+# Incoming e-mails need to be handed over (piped) to mlmmj-receive(1) by
+# the MTA, see http://mlmmj.org/docs/readme-postfix/ to configure the
+# MTA.
+#
+# Copyright © 2014 Guilhem Moulin <guilhem@fripost.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+set -ue
+
+fail () {
+ echo Error: "$@" >&2
+ exit 1
+}
+
+spool=/var/spool/mlmmj # mlmmj's how private directory
+lib=/var/lib/mlmmj # shared by mlmmj and (partially) by the web server
+webhost=https://lists.fripost.org
+umask 0022
+
+[ $# -ge 2 -a $# -le 3 ] || { echo "Usage: $0 list owner [language]"; exit; }
+list="$1"
+owner="$2"
+lang="${3:-en}"
+
+localpart="${list%@*}"
+domainpart="${list##*@}"
+[ "$localpart" = "$list" ] && [ "$domainpart" = "$list" ] \
+&& fail "$list is not fully-qualified"
+
+[ -d "$spool/$domainpart/$localpart" -o -d "$lib/$domainpart/$localpart" ] \
+&& fail "$list exists"
+
+ls -1 /usr/share/mlmmj/text.skel | grep -qFx "$lang" \
+|| fail "Available languages: $(echo $(ls /usr/share/mlmmj/text.skel))"
+
+case "$webhost" in
+ *"/$domainpart") listurl="$webhost/$localpart/";;
+ *) listurl="$webhost/$domainpart/$localpart/";;
+esac
+
+mkdir -p -m0700 "$spool/$domainpart/$localpart"
+mkdir -p -m0750 "$lib/$domainpart/$localpart"
+
+
+# The web server has read-only access to subscribers.
+for dir in subscribers.d digesters.d nomailsubs.d; do
+ mkdir -m0750 "$lib/$domainpart/$localpart/$dir"
+ ln -s "$lib/$domainpart/$localpart/$dir" \
+ "$spool/$domainpart/$localpart/$dir"
+done
+
+# The web server can update the list configuration.
+mkdir -m2770 "$lib/$domainpart/$localpart/control"
+ln -s "$lib/$domainpart/$localpart/control" \
+ "$spool/$domainpart/$localpart/control"
+
+# Internal directories.
+for dir in incoming queue queue/discarded \
+ subconf unsubconf bounce moderation requeue; do
+ mkdir -m0700 "$spool/$domainpart/$localpart/$dir"
+done
+
+# Link to templates.
+ln -s /usr/share/mlmmj/text.skel/$lang "$spool/$domainpart/$localpart/text"
+
+# Archives are private, but web archives are public.
+mkdir -m0700 "$lib/$domainpart/$localpart/archive"
+mkdir -m0750 "$lib/$domainpart/$localpart/webarchive"
+ln -s "$lib/$domainpart/$localpart/archive" \
+ "$spool/$domainpart/$localpart/archive"
+ln -s "$lib/$domainpart/$localpart/webarchive" \
+ "$spool/$domainpart/$localpart/webarchive"
+
+# Default configuration, non-writable from the web.
+echo "$list" > "$lib/$domainpart/$localpart/control/listaddress"
+echo "$owner" > "$lib/$domainpart/$localpart/control/owner"
+# XXX: these tunables are ignored, see http://mlmmj.org/bugs/bug.php?id=51
+#echo 127.0.0.1 > "$lib/$domainpart/$localpart/control/relayhost"
+#echo 16132 > "$lib/$domainpart/$localpart/control/smtpport"
+echo month > "$lib/$domainpart/$localpart/control/archiverotate"
+
+# RFC 2369
+cat > "$lib/$domainpart/$localpart/control/customheaders" <<- EOF
+ Errors-to: $localpart+owner@$domainpart
+ Precedence: list
+ List-Id: <$localpart.$domainpart>
+ List-URL: <$listurl>
+ List-Post: <mailto:$list>
+ List-Help: <mailto:$localpart+help@$domainpart>,
+ <$listurl/>
+ List-Subscribe: <$localpart+subscribe@$domainpart>,
+ <$listurl/>
+ List-Unsubscribe: <mailto:$localpart+unsubscribe@$domainpart>,
+ <$listurl/>
+ List-Owner: <mailto:$localpart+owner@$domainpart>
+ List-Archives: <$listurl/archives/>
+ Reply-To: $list
+ X-MailingList: $list
+ X-Loop: $list
+EOF
+cat > "$lib/$domainpart/$localpart/control/delheaders" <<- EOF
+ Return-Receipt-To:
+ Disposition-Notification-To:
+ X-Confirm-Reading-To:
+ X-Pmrqc:
+EOF
+
+# Some useful default, that the user is free to change via the web
+# interface.
+cat > "$lib/$domainpart/$localpart/control/footer" <<- EOF
+ _______________________________________________
+ $localpart mailing list
+ $localpart@$domainpart
+ $listurl
+EOF
+echo "[$localpart]" > "$lib/$domainpart/$localpart/control/prefix"
+touch "$lib/$domainpart/$localpart/control/subonlypost" \
+ "$lib/$domainpart/$localpart/control/subonlyget"
+
+for control in customheaders footer prefix subonlypost subonlyget; do
+ chmod 0664 "$lib/$domainpart/$localpart/control/$control"
+done
+
+# TODO: welcome mail
diff --git a/roles/lists/files/var/lib/mlmmj/static/css/fripost.css b/roles/lists/files/var/lib/mlmmj/static/css/fripost.css
new file mode 100644
index 0000000..197eee6
--- /dev/null
+++ b/roles/lists/files/var/lib/mlmmj/static/css/fripost.css
@@ -0,0 +1,63 @@
+html, body {
+ height: 100%;
+ max-width: 1024px;
+}
+#wrap {
+ min-height: 100%;
+ height: auto;
+ margin: -1.6em auto;
+ padding: 1.6em 0;
+ width: 100%;
+}
+#main {
+ margin: 0 15px;
+ padding: 0;
+ width: auto;
+}
+#header, #footer {
+ margin: 0 15px;
+ height: 1.5em;
+ overflow: hidden;
+ padding: 0;
+}
+#header {
+ border-bottom: 1px solid #c0c0c0;
+}
+#footer {
+ border-top: 1px solid #c0c0c0;
+}
+h1.msg.subject {
+ margin-bottom: 1.5ex;
+}
+.msg.header {
+ width: auto;
+ padding: 0;
+ margin-bottom: 2ex;
+}
+.msg.header table {
+ margin: 10px;
+}
+.msg.header table tr td:first-child {
+ font-weight: bold;
+ padding-right: 1ex;
+}
+.msg.body pre {
+ background: none;
+ border: none;
+ padding: 0 10px;
+}
+.dummy {
+ list-style: none;
+ font-style: italic;
+ color: gray;
+}
+.msg.footer {
+ margin-top: 1ex;
+ padding: 10px;
+}
+.msg.footer > ul {
+ margin-bottom: 0;
+}
+#main ul.index {
+ margin: 2ex 0;
+}
diff --git a/roles/lists/handlers/main.yml b/roles/lists/handlers/main.yml
new file mode 100644
index 0000000..c27834e
--- /dev/null
+++ b/roles/lists/handlers/main.yml
@@ -0,0 +1,6 @@
+---
+- name: Restart Postfix
+ service: name=postfix state=restarted
+
+- name: Reload Postfix
+ service: name=postfix state=reloaded
diff --git a/roles/lists/tasks/mail.yml b/roles/lists/tasks/mail.yml
new file mode 100644
index 0000000..7bd471e
--- /dev/null
+++ b/roles/lists/tasks/mail.yml
@@ -0,0 +1,35 @@
+- name: Install Postfix
+ apt: pkg={{ item }}
+ with_items:
+ - postfix
+ - postfix-ldap
+
+- name: Configure Postfix
+ template: src=etc/postfix/main.cf.j2
+ dest=/etc/postfix-{{ postfix_instance[inst].name }}/main.cf
+ owner=root group=root
+ mode=0644
+ register: r
+ notify:
+ - Restart Postfix
+
+- name: Create directory /etc/postfix-.../virtual
+ file: path=/etc/postfix-{{ postfix_instance[inst].name }}/virtual
+ state=directory
+ owner=root group=root
+ mode=0755
+
+- name: Copy lookup tables
+ copy: src=etc/postfix/virtual/{{ item }}
+ dest=/etc/postfix-{{ postfix_instance[inst].name }}/virtual/{{ item }}
+ owner=root group=root
+ mode=0644
+ with_items:
+ - mailbox_domains.cf
+ - transport_lists_maps.cf
+
+- name: Start Postfix
+ service: name=postfix state=started
+ when: not r.changed
+
+- meta: flush_handlers
diff --git a/roles/lists/tasks/main.yml b/roles/lists/tasks/main.yml
new file mode 100644
index 0000000..13d3451
--- /dev/null
+++ b/roles/lists/tasks/main.yml
@@ -0,0 +1,2 @@
+- include: mail.yml tags=postfix,mail
+- include: mlmmj.yml tags=mlmmj,lists
diff --git a/roles/lists/tasks/mlmmj.yml b/roles/lists/tasks/mlmmj.yml
new file mode 100644
index 0000000..0b88d75
--- /dev/null
+++ b/roles/lists/tasks/mlmmj.yml
@@ -0,0 +1,78 @@
+- name: Install MLMMJ
+ apt: pkg={{ item }}
+ with_items:
+ - mlmmj
+
+# Weird the debian package doesn't do it by itself...
+- name: Create a user 'mlmmj'
+ user: name=mlmmj system=yes
+ home=/var/spool/mlmmj
+ shell=/bin/false
+ password=!
+ state=present
+
+- name: Add 'www-data' to the group 'mlmmj'
+ user: name=www-data groups=mlmmj append=yes
+
+- name: Create a home directory for user 'mlmmj'
+ file: path=/var/spool/mlmmj
+ state=directory
+ owner=mlmmj group=mlmmj
+ mode=0700
+
+- name: Create /var/lib/mlmmj
+ file: path=/var/lib/mlmmj
+ state=directory
+ owner=mlmmj group=mlmmj
+ mode=0750
+
+- name: Auto-maintain mlmmj's spool directory
+ copy: src=etc/cron.d/mlmmj
+ dest=/etc/cron.d/mlmmj
+ owner=root group=root
+ mode=0644
+
+- name: Copy mlmmj-newlist.sh and mhonarc-scan.sh
+ copy: src=usr/local/bin/{{ item }}
+ dest=/usr/local/bin/{{ item }}
+ owner=root group=root
+ mode=0755
+ with_items:
+ - mlmmj-newlist.sh
+ - mhonarc-scan.sh
+
+- name: Copy /etc/mhonarc.rc
+ copy: src=etc/mhonarc.rc
+ dest=/etc/mhonarc.rc
+ owner=root group=root
+ mode=0644
+
+- name: Create /var/lib/mlmmj/...
+ file: path=/var/lib/mlmmj/{{ item }}
+ state=directory
+ owner=root group=root
+ mode=0755
+ with_items:
+ - static
+ - static/css
+ - static/fonts
+
+- name: Copy CSS files
+ copy: src=var/lib/mlmmj/static/css/{{ item }}
+ dest=/var/lib/mlmmj/static/css/{{ item }}
+ owner=root group=root
+ mode=0644
+ with_items:
+ - bootstrap.min.css
+ - fripost.css
+
+- name: Copy Glyphicon Halflings
+ copy: src=var/lib/mlmmj/static/fonts/{{ item }}
+ dest=/var/lib/mlmmj/static/fonts/{{ item }}
+ owner=root group=root
+ mode=0644
+ with_items:
+ - glyphicons-halflings-regular.eot
+ - glyphicons-halflings-regular.svg
+ - glyphicons-halflings-regular.ttf
+ - glyphicons-halflings-regular.woff
diff --git a/roles/lists/templates/etc/postfix/main.cf.j2 b/roles/lists/templates/etc/postfix/main.cf.j2
new file mode 100644
index 0000000..dff9d0a
--- /dev/null
+++ b/roles/lists/templates/etc/postfix/main.cf.j2
@@ -0,0 +1,76 @@
+########################################################################
+# Lists configuration
+#
+# {{ ansible_managed }}
+# Do NOT edit this file directly!
+
+smtpd_banner = $myhostname ESMTP $mail_name (Debian/GNU)
+biff = no
+readme_directory = no
+mail_owner = postfix
+
+delay_warning_time = 4h
+maximal_queue_lifetime = 5d
+
+myorigin = /etc/mailname
+myhostname = lists{{ listsno | default('') }}.$mydomain
+mydomain = {{ ansible_domain }}
+append_dot_mydomain = no
+
+# Turn off all TCP/IP listener ports except that necessary for the list server.
+# XXX: mlmmj is not compatible with the MX, see
+# http://mlmmj.org/bugs/bug.php?id=51
+master_service_disable = !127.0.0.1:smtp.inet !2527.inet inet
+
+queue_directory = /var/spool/postfix-{{ postfix_instance[inst].name }}
+data_directory = /var/lib/postfix-{{ postfix_instance[inst].name }}
+multi_instance_group = {{ postfix_instance[inst].group | default('') }}
+multi_instance_name = postfix-{{ postfix_instance[inst].name }}
+multi_instance_enable = yes
+
+# This server is a Mail Delivery Agent
+mynetworks_style = host
+inet_interfaces = 172.16.0.1
+{% if 'MX' in group_names %}
+ 127.0.0.1
+{% endif %}
+inet_protocols = ipv4
+
+# No local delivery
+mydestination =
+local_transport = error:5.1.1 Mailbox unavailable
+alias_maps =
+alias_database =
+local_recipient_maps =
+
+message_size_limit = 67108864
+recipient_delimiter = +
+
+# Forward everything to our internal mailhub
+{% if 'MTA-out' in group_names %}
+relayhost = [127.0.0.1]:{{ MTA_out.port }}
+{% else %}
+relayhost = [{{ MTA_out.host }}]:{{ MTA_out.port }}
+{% endif %}
+relay_domains =
+
+# Virtual transport (the alias resolution is already done by the MX:es)
+transport_maps = ldap:$config_directory/virtual/transport_lists_maps.cf
+mlmmj_destination_recipient_limit = 1
+
+# Don't rewrite remote headers
+local_header_rewrite_clients =
+# Avoid splitting the envelope and scanning messages multiple times
+smtp_destination_recipient_limit = 1000
+# Tolerate occasional high latency
+smtp_data_done_timeout = 1200s
+smtpd_timeout = 1200s
+
+# Tunnel everything through IPSec
+smtp_tls_security_level = none
+{% if 'MTA-out' in group_names %}
+smtp_bind_address = 127.0.0.1
+{% else %}
+smtp_bind_address = 172.16.0.1
+{% endif %}
+smtpd_tls_security_level = none