commit afefedf4d7e747cf2a6a1f6834df487b1f28c48d
Author: Serghei Mihai <smihai@entrouvert.com>
Date:   Fri May 6 17:21:56 2016 +0200

    announces send command (#10805)
    
    Ran as cron job every 5 minutes

diff --git a/corbo/management/__init__.py b/corbo/management/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/corbo/management/commands/__init__.py b/corbo/management/commands/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/corbo/management/commands/send_announces.py b/corbo/management/commands/send_announces.py
new file mode 100644
index 0000000..64cc358
--- /dev/null
+++ b/corbo/management/commands/send_announces.py
@@ -0,0 +1,28 @@
+# corbo - Announces Manager
+# Copyright (C) 2016 Entr'ouvert
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Affero 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from django.utils import timezone
+from django.core.management.base import BaseCommand, CommandError
+
+from corbo.models import Broadcast
+
+class Command(BaseCommand):
+
+    def handle(self, *args, **kwargs):
+        to_deliver = Broadcast.objects.filter(announce__publication_time__lte=timezone.now(),
+                                deliver_time__isnull=True)
+        for broadcast in to_deliver:
+            broadcast.send()
diff --git a/corbo/models.py b/corbo/models.py
index 3c22ec9..f7fbde9 100644
--- a/corbo/models.py
+++ b/corbo/models.py
@@ -1,6 +1,13 @@
+from datetime import datetime
+import logging
+from html2text import HTML2Text
+from emails.django import Message
+from lxml.etree import HTML as HTMLTree
+
 from django.utils import timezone
 from django.conf import settings
 from django.db import models
+from django.core.files.storage import DefaultStorage
 from django.utils.translation import ugettext_lazy as _
 
 from ckeditor.fields import RichTextField
@@ -10,6 +17,8 @@ channel_choices = (
     ('homepage', _('Homepage'))
 )
 
+logger = logging.getLogger(__name__)
+
 class Category(models.Model):
     name = models.CharField(max_length=64, blank=False, null=False)
     ctime = models.DateTimeField(auto_now_add=True)
@@ -62,6 +71,36 @@ class Broadcast(models.Model):
                 id=self.announce.id, time=self.deliver_time)
         return u'announce {id} to deliver'.format(id=self.announce.id)
 
+    def send(self):
+        subscriptions = self.announce.category.subscription_set.all()
+        total_sent = 0
+        handler = HTML2Text()
+        m = Message(html=self.announce.text, subject=self.announce.title,
+                    text=handler.handle(self.announce.text),
+                    mail_from=settings.CORBO_DEFAULT_FROM_EMAIL)
+        html_tree = HTMLTree(self.announce.text)
+        storage = DefaultStorage()
+        for img in html_tree.xpath('//img/@src'):
+            img_path = img.lstrip(storage.base_url)
+            m.attach(filename=img, data=storage.open(img_path))
+            m.attachments[img].is_inline = True
+        m.transformer.synchronize_inline_images()
+        m.transformer.load_and_transform()
+        m.transformer.save()
+        for s in subscriptions:
+            if not s.identifier:
+                continue
+            sent = m.send(to=s.identifier)
+            if sent:
+                total_sent += 1
+                logger.info('Announce "%s" sent to %s', self.announce.title, s.identifier)
+            else:
+                logger.warning('Error occured while sending announce "%s" to %s.',
+                               self.announce.title, s.identifier)
+        self.result = total_sent
+        self.deliver_time = timezone.now()
+        self.save()
+
     class Meta:
         verbose_name = _('sent')
         ordering = ('-deliver_time',)
diff --git a/corbo/settings.py b/corbo/settings.py
index 654c98c..a70dacc 100644
--- a/corbo/settings.py
+++ b/corbo/settings.py
@@ -104,6 +104,9 @@ RSS_DESCRIPTION = ''
 RSS_LINK = ''
 RSS_LINK_TEMPLATE = '/#announce{0}'
 
+# default mass emails expeditor
+CORBO_DEFAULT_FROM_EMAIL = 'webmaster@localhost'
+
 # django-mellon settings
 MELLON_ATTRIBUTE_MAPPING = {
     'username': '{attributes[username][0]}',
diff --git a/debian/corbo.cron.d b/debian/corbo.cron.d
new file mode 100644
index 0000000..46d2506
--- /dev/null
+++ b/debian/corbo.cron.d
@@ -0,0 +1,3 @@
+PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
+
+*/5 * * * * corbo corbo-manage tenant_command send_announces --all-tenants
diff --git a/requirements.txt b/requirements.txt
index 2f6bdfc..2d0946c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,7 @@
 Django>=1.7, <1.8
 django-ckeditor<4.5.3
 djangorestframework
+html2text
+emails
 -e git+http://repos.entrouvert.org/gadjo.git/#egg=gadjo
+
diff --git a/setup.py b/setup.py
index e59a798..2254dba 100644
--- a/setup.py
+++ b/setup.py
@@ -96,7 +96,10 @@ setup(
     install_requires=['django>=1.7, <1.8',
         'django-ckeditor<4.5.3',
         'djangorestframework',
-        'gadjo'
+        'html2text',
+        'gadjo',
+        'emails',
+        'lxml',
         ],
     zip_safe=False,
     cmdclass={
diff --git a/tests/media/logo.png b/tests/media/logo.png
new file mode 100644
index 0000000..fb954dd
Binary files /dev/null and b/tests/media/logo.png differ
diff --git a/tests/test_emailing.py b/tests/test_emailing.py
new file mode 100644
index 0000000..e7a134d
--- /dev/null
+++ b/tests/test_emailing.py
@@ -0,0 +1,93 @@
+import pytest
+import json
+from uuid import uuid4
+import os
+
+from django.core.urlresolvers import reverse
+from django.utils.http import urlencode
+from django.core import mail
+from django.utils import timezone
+from django.core.files.storage import DefaultStorage
+
+from corbo.models import Category, Announce, Subscription, Broadcast
+from corbo.models import channel_choices
+
+pytestmark = pytest.mark.django_db
+
+CATEGORIES = ('Alerts', 'News')
+
+
+@pytest.fixture
+def categories():
+    categories = []
+    for category in CATEGORIES:
+        c, created = Category.objects.get_or_create(name=category)
+        categories.append(c)
+    return categories
+
+@pytest.fixture
+def announces():
+    announces = []
+    for category in Category.objects.all():
+        a = Announce.objects.create(category=category, title='Announce 1',
+                                    publication_time=timezone.now(),
+                                    text='<h2>Announce 1</h2>')
+        Broadcast.objects.create(announce=a)
+        announces.append(a)
+        a = Announce.objects.create(category=category, title='Announce 2',
+                                    publication_time=timezone.now(),
+                                    text='<h2>Announce 2</h2>')
+        Broadcast.objects.create(announce=a)
+        announces.append(a)
+    return announces
+
+def test_emailing_with_no_subscriptions(app, categories, announces):
+    for announce in announces:
+        broadcast = Broadcast.objects.get(announce=announce)
+        broadcast.send()
+        assert not broadcast.result
+    assert not mail.outbox
+
+def test_send_email(app, categories, announces):
+    for category in categories:
+        uuid = uuid4()
+        s = Subscription.objects.create(category=category,
+                    identifier='%s@example.net' % uuid, uuid=uuid)
+    for announce in announces:
+        broadcast= Broadcast.objects.get(announce=announce)
+        broadcast.send()
+        assert broadcast.result
+        assert mail.outbox
+
+def test_check_inline_css(app, categories, announces):
+    for announce in announces:
+        announce.text = '<style type="text/css">h2 {color: #F00}</style>' + announce.text
+        announce.save()
+        uuid = uuid4()
+        s = Subscription.objects.create(category=announce.category,
+                    identifier='%s@example.net' % uuid, uuid=uuid)
+        broadcast = Broadcast.objects.get(announce=announce)
+        broadcast.send()
+        assert broadcast.result
+        assert mail.outbox
+        assert 'h2 style="color:#F00"' in mail.outbox[0].html
+        mail.outbox = []
+
+def test_check_inline_images(app, categories, announces):
+    storage = DefaultStorage()
+    media_path = os.path.join(os.path.dirname(__file__), 'media')
+    image_name = 'logo.png'
+    storage.save(image_name, file(os.path.join(media_path, image_name)))
+    for announce in announces:
+        announce.text = announce.text + '<img src="/media/%s" />' % image_name
+        announce.save()
+        uuid = uuid4()
+        s = Subscription.objects.create(category=announce.category,
+                    identifier='%s@example.net' % uuid, uuid=uuid)
+        broadcast = Broadcast.objects.get(announce=announce)
+        broadcast.send()
+        assert broadcast.result
+        assert mail.outbox
+        assert storage.url(image_name) in mail.outbox[0].attachments.keys()
+        mail.outbox = []
+    storage.delete(image_name)
