Index: main/waeup.kofa/trunk/CHANGES.txt
===================================================================
--- main/waeup.kofa/trunk/CHANGES.txt	(revision 16298)
+++ main/waeup.kofa/trunk/CHANGES.txt	(revision 16299)
@@ -4,4 +4,6 @@
 1.6.1.dev0 (unreleased)
 =======================
+
+* Implement bulk emailing.
 
 * Make login trouble links more conspicuous.
Index: main/waeup.kofa/trunk/src/waeup/kofa/interfaces.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/interfaces.py	(revision 16298)
+++ main/waeup.kofa/trunk/src/waeup/kofa/interfaces.py	(revision 16299)
@@ -508,4 +508,10 @@
         )
 
+    bcc_to = schema.Text(
+        title = _(u'Bcc to:'),
+        required = True,
+        #readonly = True,
+        )
+
     subject = schema.TextLine(
         title = _(u'Subject:'),
@@ -519,4 +525,5 @@
         title = _(u'Text:'),
         required = True,)
+
 
 class IKofaPrincipalInfo(IPrincipalInfo):
Index: main/waeup.kofa/trunk/src/waeup/kofa/smtp.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/smtp.py	(revision 16298)
+++ main/waeup.kofa/trunk/src/waeup/kofa/smtp.py	(revision 16299)
@@ -160,5 +160,5 @@
 
 def send_mail(from_name, from_addr, rcpt_name, rcpt_addrs,
-              subject, body, config=None, cc=False):
+              subject, body, config=None, cc=None, bcc=None):
     """Send mail.
 
@@ -173,8 +173,4 @@
     The from-address set here will be: `FROM_ADDRESS` as set above.
 
-    ``cc`` tells whether we want the from-address to be CCed. This is
-    not the case by default as we easily act as an open relay
-    otherwise.
-
     XXX: The hard-coded from-address should be changable or be
          determined smarter by looking up a FQDN or similar.
@@ -184,4 +180,5 @@
     rcpt_addrs = rcpt_addrs.replace(' ', '').split(',')
     body_to = ''
+    from_addr = from_addr.split(',')[0]
     for email in rcpt_addrs:
         body_to += '%s, ' % encode_address(email, rcpt_name)
@@ -192,5 +189,7 @@
     body["To"] = body_to.strip(', ')
     if cc:
-        body["Cc"] = reply_addr
+        body["Cc"] = cc
+    if bcc:
+        body["Bcc"] = bcc
     body["Reply-To"] = reply_addr
     body["Subject"] = encode_header_item(subject)
Index: main/waeup.kofa/trunk/src/waeup/kofa/students/browser.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/students/browser.py	(revision 16298)
+++ main/waeup.kofa/trunk/src/waeup/kofa/students/browser.py	(revision 16299)
@@ -36,4 +36,5 @@
 from zope.security import checkPermission
 from zope.securitypolicy.interfaces import IPrincipalRoleManager
+from waeup.kofa.smtp import encode_address
 from waeup.kofa.accesscodes import invalidate_accesscode, get_access_code
 from waeup.kofa.accesscodes.workflow import USED
@@ -309,4 +310,5 @@
     search_button = _('Find student(s)')
     remove_button = _('Remove selected')
+    send_email_button = _('Send bulk email')
     doclink = DOCLINK + '/students.html'
 
@@ -340,5 +342,5 @@
             if not self.hitlist:
                 self.flash(_('No student found.'), type="warning")
-            if 'remove' in form:
+            if 'remove' in form or 'send_email' in form:
                 self.flash(_('No item selected.'), type="warning")
             return
@@ -346,9 +348,23 @@
         if isinstance(entries, basestring):
             entries = [entries]
+        entries.sort()
         deleted = []
+        emails_tobesent = ''
         for entry in entries:
             if 'remove' in form:
                 del self.context[entry]
                 deleted.append(entry)
+            if 'send_email' in form and self.context[entry].email:
+                emails_tobesent += '%s,' % encode_address(
+                    self.context[entry].email,
+                    self.context[entry].student_id)
+        if 'send_email' in form and not len(emails_tobesent):
+            self.flash(_('No email address found.'), type="warning")
+            return
+        if len(emails_tobesent):
+            args = {'bcc_to': emails_tobesent.strip(',')}
+            self.redirect(self.url(self.context) +
+                '/send_bulk_email?%s' % urlencode(args))
+            return
         self.hitlist = search(query=self.searchterm,
             searchtype=self.searchtype, view=self)
@@ -521,4 +537,38 @@
                 self.config.name,
                 data['body'],data['subject'])
+        if success:
+            self.flash(_('Your message has been sent.'))
+        else:
+            self.flash(_('An smtp server error occurred.'), type="danger")
+        return
+
+class SendBulEmailsFormPage(ContactAdminFormPage):
+    grok.context(IStudentsContainer)
+    grok.name('send_bulk_email')
+    grok.require('waeup.manageStudent')
+    pnav = 4
+    form_fields = grok.AutoFields(IContactForm).select('subject', 'body', 'bcc_to')
+    label = _('Send bulk email')
+
+    def update(self, bcc_to=u''):
+        self.form_fields.get('bcc_to').field.default = bcc_to
+        return
+
+    @action('Send message now', style='primary')
+    def send(self, *args, **data):
+        try:
+            email = self.request.principal.email
+        except AttributeError:
+            email = self.config.email_admin
+        usertype = getattr(self.request.principal,
+                           'user_type', 'system').title()
+        kofa_utils = getUtility(IKofaUtils)
+        success = kofa_utils.sendContactForm(
+                self.request.principal.title,email,
+                self.request.principal.title,email, # sent to the sender's address
+                self.request.principal.id,usertype,
+                self.config.name,
+                data['body'],data['subject'],
+                data['bcc_to'])
         if success:
             self.flash(_('Your message has been sent.'))
Index: main/waeup.kofa/trunk/src/waeup/kofa/students/browser_templates/containermanagepage.pt
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/students/browser_templates/containermanagepage.pt	(revision 16298)
+++ main/waeup.kofa/trunk/src/waeup/kofa/students/browser_templates/containermanagepage.pt	(revision 16299)
@@ -75,5 +75,8 @@
     <input type="submit" name="remove"
            tal:attributes="value view/remove_button" class="btn btn-default"
-           onclick="return confirmPost('Are you sure?')"/>
+           onclick="return window.confirm('Are you sure?')"/>
+    <input type="submit" name="send_email"
+           tal:attributes="value view/send_email_button" class="btn btn-default"
+           onclick="return window.confirm('Are you sure?')"/>
 
   </div>
Index: main/waeup.kofa/trunk/src/waeup/kofa/students/tests/test_browser.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/students/tests/test_browser.py	(revision 16298)
+++ main/waeup.kofa/trunk/src/waeup/kofa/students/tests/test_browser.py	(revision 16299)
@@ -90,4 +90,5 @@
     def setUp(self):
         super(StudentsFullSetup, self).setUp()
+        self.setup_logging()
 
         # Setup a sample site for each test
@@ -252,4 +253,19 @@
 
     layer = FunctionalLayer
+
+    def setup_logging(self):
+        # setup a log-handler that catches all fake mailer output
+        self.stream = StringIO()
+        handler = logging.StreamHandler(self.stream)
+        logger = logging.getLogger('test.smtp')
+        logger.addHandler(handler)
+        logger.setLevel(logging.INFO)
+        return
+
+    def get_fake_smtp_output(self):
+        # get output generated by fake mailer
+        self.stream.flush()
+        self.stream.seek(0)
+        return self.stream.read()
 
     def test_anonymous_access(self):
@@ -432,4 +448,58 @@
         self.assertTrue('No student found' in self.browser.contents)
         return
+
+    def test_send_bulk_emails(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.manage_container_path)
+        self.browser.getLink("Add student").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.add_student_path)
+        self.browser.getControl(name="form.firstname").value = 'Bob'
+        self.browser.getControl(name="form.lastname").value = 'Tester'
+        self.browser.getControl(name="form.reg_number").value = '1234'
+        self.browser.getControl("Create student").click()
+        self.assertTrue('Student record created' in self.browser.contents)
+        self.browser.getLink("Manage").click()
+        self.browser.getControl(name="form.email").value = 'uli@uni.ng'
+        self.browser.getControl(name="form.sex").value = ['m']
+        self.browser.getControl("Save").click()
+        # We can find Anna and Bob
+        self.browser.open(self.manage_container_path)
+        self.browser.getControl(name="searchtype").value = ['student_id']
+        self.browser.getControl(name="searchterm").value = '*'
+        self.browser.getControl("Find student(s)").click()
+        self.assertTrue('Anna Tester' in self.browser.contents)
+        self.assertTrue('Bob Tester' in self.browser.contents)
+        # We can send bulk emails
+        ctrl = self.browser.getControl(name='entries')
+        ctrl.getControl(value='K1000000').selected = True
+        ctrl.getControl(value='K1000001').selected = True
+        self.browser.getControl("Send bulk", index=0).click()
+        self.assertTrue('K1000000 &lt;aa@aa.ng&gt;,K1000001 &lt;uli@uni.ng&gt;'
+            in self.browser.contents)
+        self.browser.getControl(name="form.subject").value = 'Test'
+        self.browser.getControl(name="form.body").value = 'Hello world'
+        self.browser.getControl("Send message").click()
+        self.assertTrue('Your message has been sent' in self.browser.contents)
+        self.assertMatches(
+        u'Sending email from no-reply@waeup.org to contact@waeup.org:\n'
+        u'Message:\n'
+        u'msg: MIME-Version: 1.0\n'
+        u'msg: Content-Type: text/plain; charset="us-ascii"\n'
+        u'msg: Content-Transfer-Encoding: 7bit\n'
+        u'msg: From: Manager <no-reply@waeup.org>\n'
+        u'msg: To: Manager <contact@waeup.org>\n'
+        u'msg: Bcc: K1000000 <aa@aa.ng>,K1000001 <uli@uni.ng>\n'
+        u'msg: Reply-To: Manager <contact@waeup.org>\n'
+        u'msg: Subject: Test\n'
+        u'msg: \n'
+        u'msg: Hello world\n'
+        u'msg: \n'
+        u'msg: ---\n'
+        u'msg: Manager (id: zope.mgr)\n'
+        u'msg: Sample University\n'
+        u'msg: \n',
+            self.get_fake_smtp_output()
+            )
 
     def test_add_graduated_students(self):
Index: main/waeup.kofa/trunk/src/waeup/kofa/tests/test_smtp.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/tests/test_smtp.py	(revision 16298)
+++ main/waeup.kofa/trunk/src/waeup/kofa/tests/test_smtp.py	(revision 16299)
@@ -259,6 +259,8 @@
             u'A recipient', u'recpt@example.com',
             u'A subject',
-            u'This is a test mail.',
-            cc=True)
+            u'This is a test mail.',None,
+            u'A cc recipient <recpt@example.com>',
+            u'A bcc recipient <recpt@example.com>,A 2nd bcc recipient <recpt@example.com>',
+            )
         self.assertEqual(mail_id, 'fake-message-id@example.com')
         self.assertEqual(
@@ -272,5 +274,7 @@
              u'msg: From: A sender <no-reply@waeup.org>',
              u'msg: To: A recipient <recpt@example.com>',
-             u'msg: Cc: A sender <sender@example.com>',
+             u'msg: Cc: A cc recipient <recpt@example.com>',
+             u'msg: Bcc: A bcc recipient <recpt@example.com>,',
+             u'msg:  A 2nd bcc recipient <recpt@example.com>',
              u'msg: Reply-To: A sender <sender@example.com>',
              u'msg: Subject: A subject',
Index: main/waeup.kofa/trunk/src/waeup/kofa/utils/utils.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/utils/utils.py	(revision 16298)
+++ main/waeup.kofa/trunk/src/waeup/kofa/utils/utils.py	(revision 16299)
@@ -35,5 +35,6 @@
 def send_mail(from_name, from_addr,
               rcpt_name, rcpt_addr,
-              subject, body, config):
+              subject, body, config,
+              cc=None, bcc=None):
     """Wrapper for the real SMTP functionality in :mod:`waeup.kofa.smtp`.
 
@@ -42,5 +43,5 @@
     mail_id = send_mail_internally(
         from_name, from_addr, rcpt_name, rcpt_addr,
-        subject, body, config)
+        subject, body, config, cc, bcc)
     return True
 
@@ -247,5 +248,6 @@
 
     def sendContactForm(self, from_name, from_addr, rcpt_name, rcpt_addr,
-                        from_username, usertype, portal, body, subject):
+                        from_username, usertype, portal, body, subject,
+                        bcc_to=None):
         """Send an email with data provided by forms.
         """
@@ -269,5 +271,5 @@
         return send_mail(
             from_name, from_addr, rcpt_name, rcpt_addr,
-            subject, body, config)
+            subject, body, config, None, bcc_to)
 
     def getUsers(self):
