def encodePassword(plaintext, scheme, other=None, config=None): """Encrypt the plaintext password. """ if plaintext is None: plaintext = "" if scheme == "PBKDF2": if other: rounds, salt, raw_salt, digest = pbkdf2_unpack(other) else: raw_salt = random_.token_bytes(20) salt = h64encode(raw_salt) if config: rounds = config.PASSWORD_PBKDF2_DEFAULT_ROUNDS else: rounds = 10000 if rounds < 1000: raise PasswordValueError("invalid PBKDF2 hash (rounds too low)") raw_digest = pbkdf2(plaintext, raw_salt, rounds, 20) return "%d$%s$%s" % (rounds, salt, h64encode(raw_digest)) elif scheme == 'SSHA': if other: raw_other = b64decode(other) salt = raw_other[20:] else: # new password # variable salt length salt_len = random_.randbelow(52 - 36) + 36 salt = random_.token_bytes(salt_len) s = ssha(s2b(plaintext), salt) elif scheme == 'SHA': s = sha1(s2b(plaintext)).hexdigest() # nosec elif scheme == 'MD5': s = md5(s2b(plaintext)).hexdigest() # nosec elif scheme == 'crypt': if crypt is None: raise PasswordValueError('Unsupported encryption scheme %r' % scheme) if other is not None: salt = other else: saltchars = './0123456789' + string.ascii_letters salt = random_.choice(saltchars) + random_.choice(saltchars) s = crypt.crypt(plaintext, salt) elif scheme == 'plaintext': s = plaintext else: raise PasswordValueError('Unknown encryption scheme %r' % scheme) return s
def send_message(self, issueid, msgid, note, sendto, from_address=None, bcc_sendto=[], subject=None, crypt=False, add_headers={}, authid=None): '''Actually send the nominated message from this issue to the sendto recipients, with the note appended. It's possible to add headers to the message with the add_headers variable. ''' users = self.db.user messages = self.db.msg files = self.db.file if msgid is None: inreplyto = None messageid = None else: inreplyto = messages.get(msgid, 'inreplyto') messageid = messages.get(msgid, 'messageid') # make up a messageid if there isn't one (web edit) if not messageid: # this is an old message that didn't get a messageid, so # create one messageid = "<%s.%s.%s%s@%s>" % ( time.time(), b2s(base64.b32encode(random_.token_bytes(10))), self.classname, issueid, self.db.config['MAIL_DOMAIN']) if msgid is not None: messages.set(msgid, messageid=messageid) # compose title cn = self.classname title = self.get(issueid, 'title') or '%s message copy' % cn # figure author information if authid: pass elif msgid: authid = messages.get(msgid, 'author') else: authid = self.db.getuid() authname = users.get(authid, 'realname') if not authname: authname = users.get(authid, 'username', '') authaddr = users.get(authid, 'address', '') if authaddr and self.db.config.MAIL_ADD_AUTHOREMAIL: authaddr = " <%s>" % formataddr(('', authaddr)) elif authaddr: authaddr = "" # make the message body m = [''] # put in roundup's signature if self.db.config.EMAIL_SIGNATURE_POSITION == 'top': m.append(self.email_signature(issueid, msgid)) # add author information if authid and self.db.config.MAIL_ADD_AUTHORINFO: if msgid and len(self.get(issueid, 'messages')) == 1: m.append( _("New submission from %(authname)s%(authaddr)s:") % locals()) elif msgid: m.append( _("%(authname)s%(authaddr)s added the comment:") % locals()) else: m.append(_("Change by %(authname)s%(authaddr)s:") % locals()) m.append('') # add the content if msgid is not None: m.append(messages.get(msgid, 'content', '')) # get the files for this message message_files = [] if msgid: for fileid in messages.get(msgid, 'files'): # check the attachment size filesize = self.db.filesize('file', fileid, None) if filesize <= self.db.config.NOSY_MAX_ATTACHMENT_SIZE: message_files.append(fileid) else: base = self.db.config.TRACKER_WEB link = "".join((base, files.classname, fileid)) filename = files.get(fileid, 'name') m.append( _("File '%(filename)s' not attached - " "you can download it from %(link)s.") % locals()) # add the change note if note: m.append(note) # put in roundup's signature if self.db.config.EMAIL_SIGNATURE_POSITION == 'bottom': m.append(self.email_signature(issueid, msgid)) # figure the encoding charset = getattr(self.db.config, 'EMAIL_CHARSET', 'utf-8') # construct the content and convert to unicode object body = s2u('\n'.join(m)) # make sure the To line is always the same (for testing mostly) sendto.sort() # make sure we have a from address if from_address is None: from_address = self.db.config.TRACKER_EMAIL # additional bit for after the From: "name" from_tag = getattr(self.db.config, 'EMAIL_FROM_TAG', '') if from_tag: from_tag = ' ' + from_tag if subject is None: subject = '[%s%s] %s' % (cn, issueid, title) author = (authname + from_tag, from_address) # send an individual message per recipient? if self.db.config.NOSY_EMAIL_SENDING != 'single': sendto = [[address] for address in sendto] else: sendto = [sendto] # tracker sender info tracker_name = s2u(self.db.config.TRACKER_NAME) tracker_name = nice_sender_header(tracker_name, from_address, charset) # now send one or more messages # TODO: I believe we have to create a new message each time as we # can't fiddle the recipients in the message ... worth testing # and/or fixing some day first = True for sendto in sendto: # create the message mailer = Mailer(self.db.config) message = mailer.get_standard_message(multipart=message_files) # set reply-to as requested by config option # TRACKER_REPLYTO_ADDRESS replyto_config = self.db.config.TRACKER_REPLYTO_ADDRESS if replyto_config: if replyto_config == "AUTHOR": # note that authaddr at this point is already # surrounded by < >, so get the original address # from the db as nice_send_header adds < > replyto_addr = nice_sender_header( authname, users.get(authid, 'address', ''), charset) else: replyto_addr = replyto_config else: replyto_addr = tracker_name message['Reply-To'] = replyto_addr # message ids if messageid: message['Message-Id'] = messageid if inreplyto: message['In-Reply-To'] = inreplyto # Generate a header for each link or multilink to # a class that has a name attribute for propname, prop in self.getprops().items(): if not isinstance(prop, (hyperdb.Link, hyperdb.Multilink)): continue cl = self.db.getclass(prop.classname) label = None if 'name' in cl.getprops(): label = 'name' if prop.msg_header_property in cl.getprops(): label = prop.msg_header_property if prop.msg_header_property == "": # if msg_header_property is set to empty string # suppress the header entirely. You can't use # 'msg_header_property == None'. None is the # default value. label = None if not label: continue if isinstance(prop, hyperdb.Link): value = self.get(issueid, propname) if value is None: continue values = [value] else: values = self.get(issueid, propname) if not values: continue values = [cl.get(v, label) for v in values] values = ', '.join(values) header = "X-Roundup-%s-%s" % (self.classname, propname) try: values.encode('ascii') message[header] = values except UnicodeError: message[header] = Header(values, charset) # Add header for main id number to make filtering # email easier than extracting from subject line. header = "X-Roundup-%s-Id" % (self.classname) values = issueid try: values.encode('ascii') message[header] = values except UnicodeError: message[header] = Header(values, charset) # Generate additional headers for k in add_headers: v = add_headers[k] try: v.encode('ascii') message[k] = v except UnicodeError: message[k] = Header(v, charset) if not inreplyto: # Default the reply to the first message msgs = self.get(issueid, 'messages') # Assume messages are sorted by increasing message number here # If the issue is just being created, and the submitter didn't # provide a message, then msgs will be empty. if msgs and msgs[0] != msgid: inreplyto = messages.get(msgs[0], 'messageid') if inreplyto: message['In-Reply-To'] = inreplyto # attach files if message_files: # first up the text as a part part = mailer.get_standard_message() part.set_payload(body, part.get_charset()) message.attach(part) for fileid in message_files: name = files.get(fileid, 'name') mime_type = (files.get(fileid, 'type') or mimetypes.guess_type(name)[0] or 'application/octet-stream') if mime_type == 'text/plain': content = files.get(fileid, 'content') part = MIMEText('') del part['Content-Transfer-Encoding'] try: enc = content.encode('ascii') part = mailer.get_text_message('us-ascii') part.set_payload(enc) except UnicodeError: # the content cannot be 7bit-encoded. # use quoted printable # XXX stuffed if we know the charset though :( part = mailer.get_text_message('utf-8') part.set_payload(content, part.get_charset()) elif mime_type == 'message/rfc822': content = files.get(fileid, 'content') main, sub = mime_type.split('/') p = FeedParser() p.feed(content) part = MIMEBase(main, sub) part.set_payload([p.close()]) else: # some other type, so encode it content = files.get(fileid, 'binary_content') main, sub = mime_type.split('/') part = MIMEBase(main, sub) part.set_payload(content) encoders.encode_base64(part) cd = 'Content-Disposition' part[cd] = 'attachment;\n filename="%s"' % name message.attach(part) else: message.set_payload(body, message.get_charset()) if crypt: send_msg = self.encrypt_to(message, sendto) else: send_msg = message mailer.set_message_attributes(send_msg, sendto, subject, author) if crypt: send_msg['Message-Id'] = message['Message-Id'] send_msg['Reply-To'] = message['Reply-To'] if message.get('In-Reply-To'): send_msg['In-Reply-To'] = message['In-Reply-To'] if sendto: mailer.smtp_send(sendto, send_msg.as_string()) if first: if crypt: # send individual bcc mails, otherwise receivers can # deduce bcc recipients from keys in message for bcc in bcc_sendto: send_msg = self.encrypt_to(message, [bcc]) send_msg['Message-Id'] = message['Message-Id'] send_msg['Reply-To'] = message['Reply-To'] if message.get('In-Reply-To'): send_msg['In-Reply-To'] = message['In-Reply-To'] mailer.smtp_send([bcc], send_msg.as_string()) elif bcc_sendto: mailer.smtp_send(bcc_sendto, send_msg.as_string()) first = False