--- /dev/null
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.18 on 2019-06-18 09:39
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('mailarchives', '0002_list_permissions'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='ResendMessage',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('registeredat', models.DateTimeField(auto_now_add=True)),
+ ('message', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='mailarchives.Message')),
+ ('sendto', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
+ ]
from django.db import models
+from django.contrib.auth.models import User
# Reason a message was hidden.
# We're intentionally putting the prefix text in the array here, since
db_table = 'listsubscribers'
+class ResendMessage(models.Model):
+ message = models.ForeignKey(Message, null=False, blank=False)
+ sendto = models.ForeignKey(User, null=False, blank=False)
+ registeredat = models.DateTimeField(null=False, blank=False, auto_now_add=True)
+
+
class ApiClient(models.Model):
apikey = models.CharField(max_length=100, null=False, blank=False)
postback = models.URLField(max_length=500, null=False, blank=False)
<a href="/message-id/raw/{{msg.messageid|urlencode}}">Raw Message</a> |
<a href="/message-id/flat/{{msg.messageid|urlencode}}">Whole Thread</a> |
<a href="/message-id/mbox/{{msg.messageid|urlencode}}">Download mbox</a>
+{%if allow_resend %}| <a href="/message-id/resend/{{msg.messageid|urlencode}}">Resend email</a>{%endif%}
</td>
</tr>
{% if not show_all %}
--- /dev/null
+{%extends "page.html"%}
+{%block title%}Resend - {{msg.subject}}{%endblock%}
+
+{%block contents%}
+<h1 class="subject">Resend - {{msg.subject}}</h1>
+<p>
+ The below message will be resent to <em>{{request.user.email}}</em>,
+ which is the email address of the account you are currently logged in with.
+</p>
+
+<p>
+ <form method="post" action="/message-id/resend/{{msg.messageid|urlencode}}">{% csrf_token %}
+ <input type="hidden" name="resend" value="1">
+ <input type="submit" value="Resend this message" class="btn btn-primary">
+ </form>
+</p>
+
+<h4>Message to resend</h4>
+{% include '_message.html' with msg=msg lists=lists show_all=True %}
+{%endblock%}
--- /dev/null
+{%extends "page.html"%}
+{%block title%}Resend - {{msg.subject}}{%endblock%}
+
+{%block contents%}
+<h1 class="subject">Resend - {{msg.subject}}</h1>
+<p>
+ The message <em>{{msg.subject}}</em> with messageid <em>{{msg.messageid}}</em>
+ has been scheduled for resending to <em>{{request.user.email}}</em>.
+</p>
+<p>
+ It will be delivered within a few minutes.
+</p>
+
+<p>
+ <a class="btn btn-primary" href="/message-id/{{msg.messageid|urlencode}}">Return to message</a>
+</p>
+{%endblock%}
from django.template import RequestContext
from django.http import HttpResponse, HttpResponseForbidden, Http404
-from django.http import StreamingHttpResponse
+from django.http import StreamingHttpResponse, HttpResponseRedirect
from django.http import HttpResponsePermanentRedirect, HttpResponseNotModified
from django.core.exceptions import PermissionDenied
from django.shortcuts import render, get_object_or_404
from django.utils.http import http_date, parse_http_date_safe
+from django.views.decorators.csrf import csrf_exempt
from django.db import connection, transaction
from django.db.models import Q
from django.conf import settings
class NavContext(object):
def __init__(self, request, listid=None, listname=None, all_groups=None, expand_groupid=None):
self.request = request
- self.ctx = {}
+ self.ctx = {
+ 'allow_resend': settings.ALLOW_RESEND,
+ }
if all_groups:
groups = copy.deepcopy(all_groups)
return _build_mbox(query, params)
+@transaction.atomic
+def resend(request, messageid):
+ if not settings.ALLOW_RESEND:
+ raise PermissionDenied("Access denied.")
+
+ if not (hasattr(request, 'user') and request.user.is_authenticated()):
+ raise ERedirect('%s?next=%s' % (settings.LOGIN_URL, request.path))
+
+ ensure_message_permissions(request, messageid)
+
+ m = get_object_or_404(Message, messageid=messageid)
+ if m.hiddenstatus:
+ raise PermissionDenied("Access denied.")
+
+ if request.method == 'POST':
+ if request.POST.get('resend', None) == '1':
+ ResendMessage(message=m, sendto=request.user).save()
+ connection.cursor().execute("NOTIFY archives_resend")
+ return HttpResponseRedirect('/message-id/resend/{0}/complete'.format(m.messageid))
+
+ lists = List.objects.extra(where=["listid IN (SELECT listid FROM list_threads WHERE threadid=%s)" % m.threadid]).order_by('listname')
+
+ return render_nav(NavContext(request, lists[0].listid, lists[0].listname), 'message_resend.html', {
+ 'msg': m,
+ 'lists': lists,
+ })
+
+
+def resend_complete(request, messageid):
+ if not settings.ALLOW_RESEND:
+ raise PermissionDenied("Access denied.")
+
+ m = get_object_or_404(Message, messageid=messageid)
+ if m.hiddenstatus:
+ raise PermissionDenied("Access denied.")
+
+ lists = List.objects.extra(where=["listid IN (SELECT listid FROM list_threads WHERE threadid=%s)" % m.threadid]).order_by('listname')
+
+ return render_nav(NavContext(request, lists[0].listid, lists[0].listname), 'resend_complete.html', {
+ 'msg': m,
+ 'lists': lists,
+ })
+
+
+@csrf_exempt
def search(request):
if not settings.PUBLIC_ARCHIVES:
# We don't support searching of non-public archives at all at this point.
INSTALLED_APPS = [
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
'archives.mailarchives',
]
SEARCH_CLIENTS = ('127.0.0.1',)
API_CLIENTS = ('127.0.0.1',)
PUBLIC_ARCHIVES = False
+ALLOW_RESEND = False
try:
from .settings_local import *
pass
# If this is a non-public site, enable middleware for handling logins etc
-if not PUBLIC_ARCHIVES:
+if ALLOW_RESEND or not PUBLIC_ARCHIVES:
MIDDLEWARE_CLASSES = [
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
] + MIDDLEWARE_CLASSES
MIDDLEWARE_CLASSES.append('archives.mailarchives.redirecthandler.RedirectMiddleware')
INSTALLED_APPS = [
- 'django.contrib.auth',
- 'django.contrib.contenttypes',
'django.contrib.sessions',
] + INSTALLED_APPS
url(r'^message-id/flat/(.+)$', archives.mailarchives.views.message_flat),
url(r'^message-id/raw/(.+)$', archives.mailarchives.views.message_raw),
url(r'^message-id/mbox/(.+)$', archives.mailarchives.views.message_mbox),
+ url(r'^message-id/resend/(.+)/complete$', archives.mailarchives.views.resend_complete),
+ url(r'^message-id/resend/(.+)$', archives.mailarchives.views.resend),
url(r'^message-id/attachment/(\d+)/.*$', archives.mailarchives.views.attachment),
url(r'^message-id/legacy/([\w-]+)/(\d+)-(\d+)/msg(\d+).php$', archives.mailarchives.views.legacy),
url(r'^message-id/(.+)$', archives.mailarchives.views.message),
url(r'^dyncss/(?P<css>base|docs).css$', archives.mailarchives.views.dynamic_css),
]
-if not settings.PUBLIC_ARCHIVES:
+if settings.ALLOW_RESEND or not settings.PUBLIC_ARCHIVES:
import archives.auth
urlpatterns += [
[varnish]
purgeurl=https://wrigleys.postgresql.org/api/varnish/purge/
+
+[smtp]
+server=localhost:9911
+heloname=localhost
+resender=noreply@example.com
+
--- /dev/null
+#!/usr/bin/python3 -u
+#
+# archives_resender.py - resend messages to authenticated users
+#
+# This script is intended to be run as a daemon.
+#
+
+
+import os
+import sys
+import select
+import smtplib
+from configparser import ConfigParser
+import psycopg2
+
+
+def process_queue(conn, sender, smtpserver, heloname):
+ with conn.cursor() as curs:
+ curs.execute("SELECT r.id, u.email, m.rawtxt FROM mailarchives_resendmessage r INNER JOIN auth_user u ON u.id=r.sendto_id INNER JOIN messages m ON m.id=r.message_id ORDER BY r.id FOR UPDATE OF r LIMIT 1")
+ ll = curs.fetchall()
+ if len(ll) == 0:
+ conn.rollback()
+ return False
+
+ recipient = ll[0][1]
+ contents = ll[0][2]
+
+ try:
+ # Actually resend! New SMTP connection for each message because we're not sending
+ # that many.
+ smtp = smtplib.SMTP(smtpserver, local_hostname=heloname)
+ smtp.sendmail(sender, recipient, contents)
+ smtp.close()
+ except Exception as e:
+ sys.stderr.write("Error sending email to {0}: {1}\n".format(recipient, e))
+
+ # Fall through and just delete the email, we never make more than one attempt
+
+ curs.execute("DELETE FROM mailarchives_resendmessage WHERE id=%(id)s", {
+ 'id': ll[0][0],
+ })
+ conn.commit()
+ return True
+
+
+if __name__ == "__main__":
+ cfg = ConfigParser()
+ cfg.read(os.path.join(os.path.realpath(os.path.dirname(sys.argv[0])), '../loader', 'archives.ini'))
+ if not cfg.has_option('smtp', 'server'):
+ print("Must specify server under smtp in configuration")
+ sys.exit(1)
+ if not cfg.has_option('smtp', 'heloname'):
+ print("Must specify heloname under smtp in configuration")
+ sys.exit(1)
+ if not cfg.has_option('smtp', 'resender'):
+ print("Must specify resender under smtp in configuration")
+ sys.exit(1)
+
+ smtpserver = cfg.get('smtp', 'server')
+ heloname = cfg.get('smtp', 'heloname')
+ sender = cfg.get('smtp', 'resender')
+
+ conn = psycopg2.connect(cfg.get('db', 'connstr') + ' application_name=archives_resender')
+
+ curs = conn.cursor()
+
+ curs.execute("LISTEN archives_resend")
+ conn.commit()
+
+ while True:
+ # Process everything in the queue now
+ while True:
+ if not process_queue(conn, sender, smtpserver, heloname):
+ break
+
+ # Wait for a NOTIFY. Poll every 5 minutes.
+ select.select([conn], [], [], 5 * 60)
+
+ # Eat up all notifications, since we're just going to process
+ # all pending messages until the queue is empty.
+ conn.poll()
+ while conn.notifies:
+ conn.notifies.pop()
--- /dev/null
+[Unit]
+Description=pgarchives: remail resender
+After=postgresql.service
+
+[Service]
+ExecStart=/some/where/resender/archives_resendewr.py
+WorkingDirectory=/some/where/resender
+Restart=always
+RestartSec=30
+User=www-data
+Group=www-data
+
+[Install]
+WantedBy=multi-user.target