3030from email .Header import Header
3131from email .MIMEText import MIMEText
3232from email .MIMEBase import MIMEBase
33+ from email .MIMEMultipart import MIMEMultipart
3334
3435from anypy .email_ import FeedParser
3536
3637from roundup import password , date , hyperdb
3738from roundup .i18n import _
39+ from roundup .hyperdb import iter_roles
3840
39- # MessageSendError is imported for backwards compatibility
4041from 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+
4350class 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 ):
0 commit comments