Skip to content

Commit dbae47c

Browse files
author
Ralf Schlatterbeck
committed
Sending of PGP-Encrypted mail to all users or selected users (via roles)...
...is now working. (Ralf)
1 parent 650667c commit dbae47c

File tree

5 files changed

+320
-36
lines changed

5 files changed

+320
-36
lines changed

CHANGES.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ Features:
1313
- Allow to turn off translation of generated html options in menu method
1414
of LinkHTMLProperty and MultilinkHTMLProperty -- default is
1515
translation as it used to be (Ralf)
16+
- Sending of PGP-Encrypted mail to all users or selected users (via
17+
roles) is now working. (Ralf)
1618

1719
Fixed:
1820

@@ -32,9 +34,7 @@ Fixed:
3234
- PGP support is again working (pyme API has changed significantly) and
3335
we now have a regression test. We now take care that bounce-messages
3436
for incoming encrypted mails or mails where the policy dictates that
35-
outgoing traffic should be encrypted is actually pgp-encrypted. Note
36-
that the new pgp encrypt option for outgoing mails works only for
37-
bounces for now. (Ralf)
37+
outgoing traffic should be encrypted is actually pgp-encrypted. (Ralf)
3838

3939
2011-07-15 1.4.19 (r4638)
4040

roundup/roundupdb.py

Lines changed: 100 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,23 @@
3030
from email.Header import Header
3131
from email.MIMEText import MIMEText
3232
from email.MIMEBase import MIMEBase
33+
from email.MIMEMultipart import MIMEMultipart
3334

3435
from anypy.email_ import FeedParser
3536

3637
from roundup import password, date, hyperdb
3738
from roundup.i18n import _
39+
from roundup.hyperdb import iter_roles
3840

39-
# MessageSendError is imported for backwards compatibility
4041
from roundup.mailer import Mailer, MessageSendError, encode_quopri, \
4142
nice_sender_header
4243

44+
try:
45+
import pyme, pyme.core
46+
except ImportError:
47+
pyme = None
48+
49+
4350
class Database:
4451

4552
# remember the journal uid for the current journaltag so that:
@@ -212,14 +219,26 @@ def nosymessage(self, issueid, msgid, oldvalues, whichnosy='nosy',
212219
The "bcc" argument also indicates additional recipients to send the
213220
message to that may not be specified in the message's recipients
214221
list. These recipients will not be included in the To: or Cc:
215-
address lists.
222+
address lists. Note that the list of bcc users *is* updated in
223+
the recipient list of the message, so this field has to be
224+
protected (using appropriate permissions), otherwise the bcc
225+
will be decuceable for users who have web access to the tracker.
216226
217227
The cc_emails and bcc_emails arguments take a list of additional
218228
recipient email addresses (just the mail address not roundup users)
219-
this can be useful for sending to additional email addresses which are no
220-
roundup users. These arguments are currently not used by roundups
221-
nosyreaction but can be used by customized (nosy-)reactors.
229+
this can be useful for sending to additional email addresses
230+
which are no roundup users. These arguments are currently not
231+
used by roundups nosyreaction but can be used by customized
232+
(nosy-)reactors.
233+
234+
A note on encryption: If pgp encryption for outgoing mails is
235+
turned on in the configuration and no specific pgp roles are
236+
defined, we try to send encrypted mail to *all* users
237+
*including* cc, bcc, cc_emails and bcc_emails and this might
238+
fail if not all the keys are available in roundups keyring.
222239
"""
240+
encrypt = self.db.config.PGP_ENABLE and self.db.config.PGP_ENCRYPT
241+
pgproles = self.db.config.PGP_ROLES
223242
if msgid:
224243
authid = self.db.msg.get(msgid, 'author')
225244
recipients = self.db.msg.get(msgid, 'recipients', [])
@@ -228,8 +247,8 @@ def nosymessage(self, issueid, msgid, oldvalues, whichnosy='nosy',
228247
authid = None
229248
recipients = []
230249

231-
sendto = []
232-
bcc_sendto = []
250+
sendto = dict (plain = [], crypt = [])
251+
bcc_sendto = dict (plain = [], crypt = [])
233252
seen_message = {}
234253
for recipient in recipients:
235254
seen_message[recipient] = 1
@@ -238,7 +257,10 @@ def add_recipient(userid, to):
238257
""" make sure they have an address """
239258
address = self.db.user.get(userid, 'address')
240259
if address:
241-
to.append(address)
260+
ciphered = encrypt and (not pgproles or
261+
self.db.user.has_role(userid, *iter_roles(pgproles)))
262+
type = ['plain', 'crypt'][ciphered]
263+
to[type].append(address)
242264
recipients.append(userid)
243265

244266
def good_recipient(userid):
@@ -273,13 +295,19 @@ def good_recipient(userid):
273295
for userid in cc + self.get(issueid, whichnosy):
274296
if good_recipient(userid):
275297
add_recipient(userid, sendto)
276-
sendto.extend (cc_emails)
298+
if encrypt and not pgproles:
299+
sendto['crypt'].extend (cc_emails)
300+
else:
301+
sendto['plain'].extend (cc_emails)
277302

278303
# now deal with bcc people.
279304
for userid in bcc:
280305
if good_recipient(userid):
281306
add_recipient(userid, bcc_sendto)
282-
bcc_sendto.extend (bcc_emails)
307+
if encrypt and not pgproles:
308+
bcc_sendto['crypt'].extend (bcc_emails)
309+
else:
310+
bcc_sendto['plain'].extend (bcc_emails)
283311

284312
if oldvalues:
285313
note = self.generateChangeNote(issueid, oldvalues)
@@ -288,17 +316,53 @@ def good_recipient(userid):
288316

289317
# If we have new recipients, update the message's recipients
290318
# and send the mail.
291-
if sendto or bcc_sendto:
319+
if sendto['plain'] or sendto['crypt']:
320+
# update msgid and recipients only if non-bcc have changed
292321
if msgid is not None:
293322
self.db.msg.set(msgid, recipients=recipients)
294-
self.send_message(issueid, msgid, note, sendto, from_address,
295-
bcc_sendto)
323+
if sendto['plain'] or bcc_sendto['plain']:
324+
self.send_message(issueid, msgid, note, sendto['plain'],
325+
from_address, bcc_sendto['plain'])
326+
if sendto['crypt'] or bcc_sendto['crypt']:
327+
self.send_message(issueid, msgid, note, sendto['crypt'],
328+
from_address, bcc_sendto['crypt'], crypt=True)
296329

297330
# backwards compatibility - don't remove
298331
sendmessage = nosymessage
299332

333+
def encrypt_to(self, message, sendto):
334+
""" Encrypt given message to sendto receivers.
335+
Returns a new RFC 3156 conforming message.
336+
"""
337+
plain = pyme.core.Data(message.as_string())
338+
cipher = pyme.core.Data()
339+
ctx = pyme.core.Context()
340+
ctx.set_armor(1)
341+
keys = []
342+
for adr in sendto:
343+
ctx.op_keylist_start(adr, 0)
344+
# only first key per email
345+
k = ctx.op_keylist_next()
346+
if k is not None:
347+
keys.append(k)
348+
else:
349+
msg = _('No key for "%(adr)s" in keyring')%locals()
350+
raise MessageSendError, msg
351+
ctx.op_keylist_end()
352+
ctx.op_encrypt(keys, 1, plain, cipher)
353+
cipher.seek(0,0)
354+
msg = MIMEMultipart('encrypted', boundary=None, _subparts=None,
355+
protocol="application/pgp-encrypted")
356+
part = MIMEBase('application', 'pgp-encrypted')
357+
part.set_payload("Version: 1\r\n")
358+
msg.attach(part)
359+
part = MIMEBase('application', 'octet-stream')
360+
part.set_payload(cipher.read())
361+
msg.attach(part)
362+
return msg
363+
300364
def send_message(self, issueid, msgid, note, sendto, from_address=None,
301-
bcc_sendto=[]):
365+
bcc_sendto=[], crypt=False):
302366
'''Actually send the nominated message from this issue to the sendto
303367
recipients, with the note appended.
304368
'''
@@ -430,7 +494,6 @@ def send_message(self, issueid, msgid, note, sendto, from_address=None,
430494
mailer = Mailer(self.db.config)
431495

432496
message = mailer.get_standard_message(multipart=message_files)
433-
mailer.set_message_attributes(message, sendto, subject, author)
434497

435498
# set reply-to to the tracker
436499
message['Reply-To'] = tracker_name
@@ -526,10 +589,29 @@ def send_message(self, issueid, msgid, note, sendto, from_address=None,
526589
message.set_payload(body)
527590
encode_quopri(message)
528591

529-
if first:
530-
mailer.smtp_send(sendto + bcc_sendto, message.as_string())
592+
if crypt:
593+
send_msg = self.encrypt_to (message, sendto)
531594
else:
532-
mailer.smtp_send(sendto, message.as_string())
595+
send_msg = message
596+
mailer.set_message_attributes(send_msg, sendto, subject, author)
597+
send_msg ['Message-Id'] = message ['Message-Id']
598+
send_msg ['Reply-To'] = message ['Reply-To']
599+
if message.get ('In-Reply-To'):
600+
send_msg ['In-Reply-To'] = message ['In-Reply-To']
601+
mailer.smtp_send(sendto, send_msg.as_string())
602+
if first:
603+
if crypt:
604+
# send individual bcc mails, otherwise receivers can
605+
# deduce bcc recipients from keys in message
606+
for bcc in bcc_sendto:
607+
send_msg = self.encrypt_to (message, [bcc])
608+
send_msg ['Message-Id'] = message ['Message-Id']
609+
send_msg ['Reply-To'] = message ['Reply-To']
610+
if message.get ('In-Reply-To'):
611+
send_msg ['In-Reply-To'] = message ['In-Reply-To']
612+
mailer.smtp_send([bcc], send_msg.as_string())
613+
elif bcc_sendto:
614+
mailer.smtp_send(bcc_sendto, send_msg.as_string())
533615
first = False
534616

535617
def email_signature(self, issueid, msgid):

test/db_test_base.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@
1818
# $Id: db_test_base.py,v 1.101 2008-08-19 01:40:59 richard Exp $
1919

2020
import unittest, os, shutil, errno, imp, sys, time, pprint, base64, os.path
21+
import gpgmelib
2122
# Python 2.3 ... 2.6 compatibility:
2223
from roundup.anypy.sets_ import set
24+
from email.parser import FeedParser
2325

2426
from roundup.hyperdb import String, Password, Link, Multilink, Date, \
2527
Interval, DatabaseError, Boolean, Number, Node
@@ -1933,6 +1935,78 @@ def dummy_snd(s, to, msg, res=res) :
19331935
roundupdb._ = old_translate_
19341936
Mailer.smtp_send = backup
19351937

1938+
def testPGPNosyMail(self) :
1939+
"""Creates one issue with two attachments, one smaller and one larger
1940+
than the set max_attachment_size. Recipients are one with and
1941+
one without encryption enabled via a gpg group.
1942+
"""
1943+
if gpgmelib.pyme is None:
1944+
print "Skipping PGPNosy test"
1945+
return
1946+
old_translate_ = roundupdb._
1947+
roundupdb._ = i18n.get_translation(language='C').gettext
1948+
db = self.db
1949+
db.config.NOSY_MAX_ATTACHMENT_SIZE = 4096
1950+
db.config['PGP_HOMEDIR'] = gpgmelib.pgphome
1951+
db.config['PGP_ROLES'] = 'pgp'
1952+
db.config['PGP_ENABLE'] = True
1953+
db.config['PGP_ENCRYPT'] = True
1954+
gpgmelib.setUpPGP()
1955+
res = []
1956+
def dummy_snd(s, to, msg, res=res) :
1957+
res.append (dict (mail_to = to, mail_msg = msg))
1958+
backup, Mailer.smtp_send = Mailer.smtp_send, dummy_snd
1959+
try :
1960+
john = db.user.create(username="john", roles='User,pgp',
1961+
address='[email protected]', realname='John Doe')
1962+
f1 = db.file.create(name="test1.txt", content="x" * 20)
1963+
f2 = db.file.create(name="test2.txt", content="y" * 5000)
1964+
m = db.msg.create(content="one two", author="admin",
1965+
files = [f1, f2])
1966+
i = db.issue.create(title='spam', files = [f1, f2],
1967+
messages = [m], nosy = [db.user.lookup("fred"), john])
1968+
1969+
db.issue.nosymessage(i, m, {})
1970+
res.sort(key=lambda x: x['mail_to'])
1971+
self.assertEqual(res[0]["mail_to"], ["[email protected]"])
1972+
self.assertEqual(res[1]["mail_to"], ["[email protected]"])
1973+
mail_msg = str(res[0]["mail_msg"])
1974+
self.assert_("From: admin" in mail_msg)
1975+
self.assert_("Subject: [issue1] spam" in mail_msg)
1976+
self.assert_("New submission from admin" in mail_msg)
1977+
self.assert_("one two" in mail_msg)
1978+
self.assert_("File 'test1.txt' not attached" not in mail_msg)
1979+
self.assert_(base64.encodestring("xxx").rstrip() in mail_msg)
1980+
self.assert_("File 'test2.txt' not attached" in mail_msg)
1981+
self.assert_(base64.encodestring("yyy").rstrip() not in mail_msg)
1982+
fp = FeedParser()
1983+
mail_msg = str(res[1]["mail_msg"])
1984+
fp.feed(mail_msg)
1985+
parts = fp.close().get_payload()
1986+
self.assertEqual(len(parts),2)
1987+
self.assertEqual(parts[0].get_payload().strip(), 'Version: 1')
1988+
crypt = gpgmelib.pyme.core.Data(parts[1].get_payload())
1989+
plain = gpgmelib.pyme.core.Data()
1990+
ctx = gpgmelib.pyme.core.Context()
1991+
res = ctx.op_decrypt(crypt, plain)
1992+
self.assertEqual(res, None)
1993+
plain.seek(0,0)
1994+
fp = FeedParser()
1995+
fp.feed(plain.read())
1996+
self.assert_("From: admin" in mail_msg)
1997+
self.assert_("Subject: [issue1] spam" in mail_msg)
1998+
mail_msg = str(fp.close())
1999+
self.assert_("New submission from admin" in mail_msg)
2000+
self.assert_("one two" in mail_msg)
2001+
self.assert_("File 'test1.txt' not attached" not in mail_msg)
2002+
self.assert_(base64.encodestring("xxx").rstrip() in mail_msg)
2003+
self.assert_("File 'test2.txt' not attached" in mail_msg)
2004+
self.assert_(base64.encodestring("yyy").rstrip() not in mail_msg)
2005+
finally :
2006+
roundupdb._ = old_translate_
2007+
Mailer.smtp_send = backup
2008+
gpgmelib.tearDownPGP()
2009+
19362010
class ROTest(MyTestCase):
19372011
def setUp(self):
19382012
# remove previous test, ignore errors

0 commit comments

Comments
 (0)