def _create_from_to_cc(cls, idx, session, trees): config = session.config ahp = AddressHeaderParser() ref_from, ref_to, ref_cc = [], [], [] result = {'from': '', 'to': [], 'cc': []} def merge_contact(ai): vcard = config.vcards.get_vcard(ai.address) if vcard: ai.merge_vcard(vcard) return ai # Parse the headers, so we know what we're working with. We prune # some of the duplicates at this stage. for addrs in [t['addresses'] for t in trees]: alist = [] for dst, addresses in ((ref_from, addrs.get('reply-to') or addrs.get('from', [])), (ref_to, addrs.get('to', [])), (ref_cc, addrs.get('cc', []))): alist += [d.address for d in dst] dst.extend([a for a in addresses if a.address not in alist]) # 1st, choose a from address. from_ai = config.vcards.choose_from_address( config, ref_from, ref_to, ref_cc) # Note: order matters! if from_ai: result['from'] = ahp.normalized(addresses=[from_ai], force_name=True) def addresses(addrs, exclude=[]): alist = [from_ai.address] if (from_ai) else [] alist += [a.address for a in exclude] return [ merge_contact(a) for a in addrs if a.address not in alist and not a.address.startswith( 'noreply@') and '@noreply' not in a.address ] # If only replying to messages sent from chosen from, then this is # a follow-up or clarification, so just use the same headers. if (from_ai and len([ e for e in ref_from if e and e.address == from_ai.address ]) == len(ref_from)): if ref_to: result['to'] = addresses(ref_to) if ref_cc: result['cc'] = addresses(ref_cc) # Else, if replying to other people: # - Construct To from the From lines, excluding own from # - Construct Cc from the To and CC lines, except new To/From else: result['to'] = addresses(ref_from) result['cc'] = addresses(ref_to + ref_cc, exclude=ref_from) return result
def _create_from_to_cc(cls, idx, session, trees): config = session.config ahp = AddressHeaderParser() ref_from, ref_to, ref_cc = [], [], [] result = {'from': '', 'to': [], 'cc': []} def merge_contact(ai): vcard = config.vcards.get_vcard(ai.address) if vcard: ai.merge_vcard(vcard) return ai # Parse the headers, so we know what we're working with. We prune # some of the duplicates at this stage. for addrs in [t['addresses'] for t in trees]: alist = [] for dst, addresses in ( (ref_from, addrs.get('reply-to') or addrs.get('from', [])), (ref_to, addrs.get('to', [])), (ref_cc, addrs.get('cc', []))): alist += [d.address for d in dst] dst.extend([a for a in addresses if a.address not in alist]) # 1st, choose a from address. from_ai = config.vcards.choose_from_address( config, ref_from, ref_to, ref_cc) # Note: order matters! if from_ai: result['from'] = ahp.normalized(addresses=[from_ai], force_name=True) def addresses(addrs, exclude=[]): alist = [from_ai.address] if (from_ai) else [] alist += [a.address for a in exclude] return [merge_contact(a) for a in addrs if a.address not in alist and not a.address.startswith('noreply@') and '@noreply' not in a.address] # If only replying to messages sent from chosen from, then this is # a follow-up or clarification, so just use the same headers. if (from_ai and len([e for e in ref_from if e and e.address == from_ai.address]) == len(ref_from)): if ref_to: result['to'] = addresses(ref_to) if ref_cc: result['cc'] = addresses(ref_cc) # Else, if replying to other people: # - Construct To from the From lines, excluding own from # - Construct Cc from the To and CC lines, except new To/From else: result['to'] = addresses(ref_from) result['cc'] = addresses(ref_to + ref_cc, exclude=ref_from) return result
def command(self): idx, vcards = self._idx(), self.session.config.vcards emails = [e for e in self.args if '@' in e] emails.extend(self.data.get('email', [])) messages = self._choose_messages( [m for m in self.args if '@' not in m] + ['=%s' % mid for mid in self.data.get('mid', [])]) for msg_idx_pos in messages: try: msg_info = idx.get_msg_at_idx_pos(msg_idx_pos) msg_emails = (idx.expand_to_list(msg_info, field=idx.MSG_TO) + idx.expand_to_list(msg_info, field=idx.MSG_CC)) emails.extend(msg_emails) if 'no_from' not in self.data: emails.append(msg_info[idx.MSG_FROM]) except ValueError: pass addrs = [ ai for ee in emails for ai in AddressHeaderParser(unicode_data=ee) ] return self._success(_('Choosing from address'), result={ 'emails': addrs, 'from': vcards.choose_from_address( self.session.config, addrs) })
def ObscureNames(hdr): """ Remove names (leaving e-mail addresses) from the To: and Cc: headers. >>> ObscureNames("Bjarni R. E. <*****@*****.**>, [email protected] (Elmer Boop)") u'<*****@*****.**>, <[email protected]>' """ from mailpile.mailutils import AddressHeaderParser return ', '.join('<%s>' % ai.address for ai in AddressHeaderParser(hdr))
def _message_to_msg_info(self, msg_idx_pos, msg_ptr, msg): msg_mid = b36(msg_idx_pos) msg_to = AddressHeaderParser(msg.get('to')) msg_cc = AddressHeaderParser(msg.get('cc')) msg_cc += AddressHeaderParser(msg.get('bcc')) return [ msg_mid, msg_ptr, # Message PTR self.get_msg_id(msg, msg_ptr), # Message ID b36(safe_message_ts(msg)), # Message timestamp safe_decode_hdr(msg, 'from'), # Message from self.compact_to_list(msg_to), # Compacted to-list self.compact_to_list(msg_cc), # Compacted cc/bcc-list b36(len(msg) // 1024), # Message size safe_decode_hdr(msg, 'subject'), # Subject self.MSG_BODY_LAZY, # Body snippets come later '', # Tags '', # Replies msg_mid ] # Thread
def _get_addresses(self, pairs, name): from mailpile.mailutils import AddressHeaderParser config = self.env.session.config addresses = [] for hdr in self._get_all(pairs, name): addresses.extend(AddressHeaderParser(unicode_data=hdr)) for ai in addresses: vcard = config.vcards.get_vcard(ai.address) if vcard: ai.merge_vcard(vcard) return addresses
def _message(self, email): tree = email.get_message_tree(want=(email.WANT_MSG_TREE_PGP + self.WANT_MSG_TREE)) email.evaluate_pgp(tree, decrypt=True) editing_strings = tree.get('editing_strings') if editing_strings: for key in ('from', 'to', 'cc', 'bcc'): if key in editing_strings: cids = self._msg_addresses(addresses=AddressHeaderParser( unicode_data=editing_strings[key])) editing_strings['%s_aids' % key] = cids for cid in cids: if cid not in self['data']['addresses']: self['data']['addresses'][cid] = self._address( cid=cid) return self._prune_msg_tree(tree)
def _create_from_to_cc(cls, idx, session, trees): config = session.config ahp = AddressHeaderParser() ref_from, ref_to, ref_cc = [], [], [] result = {'from': '', 'to': [], 'cc': []} def merge_contact(ai): vcard = session.config.vcards.get_vcard(ai.address) if vcard: ai.merge_vcard(vcard) return ai # Parse the headers, so we know what we're working with. We prune # some of the duplicates at this stage. for addrs in [t['addresses'] for t in trees]: alist = [] for dst, addresses in ( (ref_from, addrs.get('reply-to') or addrs.get('from', [])), (ref_to, addrs.get('to', [])), (ref_cc, addrs.get('cc', []))): alist += [d.address for d in dst] dst.extend([a for a in addresses if a.address not in alist]) # 1st, choose a from address. We'll use the system default if # nothing is found, but hopefully we'll find an address we # recognize in one of the headers. from_address = (session.config.prefs.default_email or session.config.profiles[0].email) profile_emails = [p.email for p in session.config.profiles if p.email] for src in (ref_from, ref_to, ref_cc): matches = [s for s in src if s.address in profile_emails] if matches: from_address = matches[0].address break result['from'] = ahp.normalized(addresses=[AddressInfo(p.email, p.name) for p in session.config.profiles if p.email == from_address], force_name=True) def addresses(addrs, exclude=[]): alist = [from_address] + [a.address for a in exclude] return ahp.normalized_addresses(addresses=[merge_contact(a) for a in addrs if a.address not in alist and not a.address.startswith('noreply@') and '@noreply' not in a.address], with_keys=True, force_name=True) # If only replying to messages sent from chosen from, then this is # a follow-up or clarification, so just use the same headers. if len([e for e in ref_from if e.address == from_address] ) == len(ref_from): if ref_to: result['to'] = addresses(ref_to) if ref_cc: result['cc'] = addresses(ref_cc) # Else, if replying to other people: # - Construct To from the From lines, excluding own from # - Construct Cc from the To and CC lines, except new To/From else: result['to'] = addresses(ref_from) result['cc'] = addresses(ref_to + ref_cc, exclude=ref_from) return result
# # In practice, we do this in two passes - first a strict pass where we try # to parse things semi-sensibly. If that fails, there is a second pass # where we try to cope with certain types of weirdness we've seen in the # wild. The wild can be pretty wild. # # This parser is NOT fully RFC2822 compliant - in particular it will get # confused by nested comments (see FIXME in tests below). # import sys import traceback from mailpile.mailutils import AddressHeaderParser as AHP ahp_tests = AHP(AHP.TEST_HEADER_DATA) print '_tokens: {0!s}'.format(ahp_tests._tokens) print '_groups: {0!s}'.format(ahp_tests._groups) print '{0!s}'.format(ahp_tests) print 'normalized: {0!s}'.format(ahp_tests.normalized()) headers, header, inheader = {}, None, False for line in sys.stdin: if inheader: if line in ('\n', '\r\n'): for hdr in ('from', 'to', 'cc'): val = headers.get(hdr, '').replace('\n', ' ').strip() if val: try: nv = AHP(val, _raise=True).normalized()
def TransformOutgoing(self, sender, rcpts, msg, **kwargs): matched = False gnupg = None sender_keyid = None # Prefer to just get everything from the profile VCard, in the # common case... profile = self.config.vcards.get_vcard(sender) if profile: sender_keyid = profile.pgp_key crypto_format = profile.crypto_format or 'none' else: crypto_format = 'none' # Parse the openpgp_header data from the crypto_format openpgp_header = [p.split(':')[-1] for p in crypto_format.split('+') if p.startswith('openpgp_header:')] if not openpgp_header: openpgp_header = self.config.prefs.openpgp_header and ['CFG'] if openpgp_header[0] != 'N' and not sender_keyid: # This is a fallback: this shouldn't happen much in normal use try: gnupg = gnupg or GnuPG(self.config, event=GetThreadEvent()) seckeys = dict([(uid["email"], fp) for fp, key in gnupg.list_secret_keys().iteritems() if key["capabilities_map"].get("encrypt") and key["capabilities_map"].get("sign") for uid in key["uids"]]) sender_keyid = seckeys.get(sender) except (KeyError, TypeError, IndexError, ValueError): traceback.print_exc() if sender_keyid and openpgp_header: preference = { 'ES': 'signencrypt', 'SE': 'signencrypt', 'E': 'encrypt', 'S': 'sign', 'N': 'unprotected', 'CFG': self.config.prefs.openpgp_header }[openpgp_header[0].upper()] msg["OpenPGP"] = ("id=%s; preference=%s" % (sender_keyid, preference)) if ('attach-pgp-pubkey' in msg and msg['attach-pgp-pubkey'][:3].lower() in ('yes', 'tru')): gnupg = gnupg or GnuPG(self.config, event=GetThreadEvent()) if sender_keyid: keys = gnupg.list_keys(selectors=[sender_keyid]) else: keys = gnupg.address_to_keys(AddressHeaderParser(sender).addresses_list()[0]) key_count = 0 for fp, key in keys.iteritems(): if not any(key["capabilities_map"].values()): continue # We should never really hit this more than once. But if we # do, should still be fine. keyid = key["keyid"] data = gnupg.get_pubkey(keyid) try: from_name = key["uids"][0]["name"] filename = _('Encryption key for %s') % from_name except: filename = _('My encryption key') if self.config.prefs.gpg_html_wrap: data = self._wrap_key_in_html(filename, data) ext = 'html' else: ext = 'asc' att = MIMEBase('application', 'pgp-keys') att.set_payload(data) encoders.encode_base64(att) del att['MIME-Version'] att.add_header('Content-Id', MakeContentID()) att.add_header('Content-Disposition', 'attachment', filename=filename + '.' + ext) att.signature_info = SignatureInfo(parent=msg.signature_info) att.encryption_info = EncryptionInfo(parent=msg.encryption_info) msg.attach(att) key_count += 1 if key_count > 0: msg['x-mp-internal-pubkeys-attached'] = "Yes" return sender, rcpts, msg, matched, True
# # In practice, we do this in two passes - first a strict pass where we try # to parse things semi-sensibly. If that fails, there is a second pass # where we try to cope with certain types of weirdness we've seen in the # wild. The wild can be pretty wild. # # This parser is NOT fully RFC2822 compliant - in particular it will get # confused by nested comments (see FIXME in tests below). # import sys import traceback from mailpile.mailutils import AddressHeaderParser as AHP ahp_tests = AHP(AHP.TEST_HEADER_DATA) print '_tokens: %s' % ahp_tests._tokens print '_groups: %s' % ahp_tests._groups print '%s' % ahp_tests print 'normalized: %s' % ahp_tests.normalized() headers, header, inheader = {}, None, False for line in sys.stdin: if inheader: if line in ('\n', '\r\n'): for hdr in ('from', 'to', 'cc'): val = headers.get(hdr, '').replace('\n', ' ').strip() if val: try: nv = AHP(val, _raise=True).normalized()
def command(self, emails=None): session, config, idx = self.session, self.session.config, self._idx() args = list(self.args) bounce_to = [] while args and '@' in args[-1]: bounce_to.append(args.pop(-1)) for rcpt in (self.data.get('to', []) + self.data.get('cc', []) + self.data.get('bcc', [])): bounce_to.extend(AddressHeaderParser(rcpt).addresses_list()) sender = self.data.get('from', [None])[0] if not sender and bounce_to: sender = idx.config.get_profile().get('email', None) if not emails: args.extend(['=%s' % mid for mid in self.data.get('mid', [])]) emails = [ self._actualize_ephemeral(i) for i in self._choose_messages(args, allow_ephemeral=True) ] # First make sure the draft tags are all gone, so other edits either # fail or complete while we wait for the lock. with GLOBAL_EDITING_LOCK: self._tag_drafts(emails, untag=True) self._tag_blank(emails, untag=True) # Process one at a time so we don't eat too much memory sent = [] missing_keys = [] locked_keys = [] for email in emails: events = [] try: msg_mid = email.get_msg_info(idx.MSG_MID) # This is a unique sending-ID. This goes in the public (meant # for debugging help) section of the event-log, so we take # care to not reveal details about the message or recipients. msg_sid = sha1b64(email.get_msg_info(idx.MSG_ID), *sorted(bounce_to))[:8] # We load up any incomplete events for sending this message # to this set of recipients. If nothing is in flight, create # a new event for tracking this operation. events = list( config.event_log.incomplete(source=self.EVENT_SOURCE, data_mid=msg_mid, data_sid=msg_sid)) if not events: events.append( config.event_log.log(source=self.EVENT_SOURCE, flags=Event.RUNNING, message=_('Sending message'), data={ 'mid': msg_mid, 'sid': msg_sid })) SendMail(session, msg_mid, [ PrepareMessage(config, email.get_msg(pgpmime=False), sender=sender, rcpts=(bounce_to or None), bounce=(True if bounce_to else False), events=events) ]) for ev in events: ev.flags = Event.COMPLETE config.event_log.log_event(ev) sent.append(email) # Encryption related failures are fatal, don't retry except (KeyLookupError, EncryptionFailureError, SignatureFailureError), exc: message = unicode(exc) session.ui.warning(message) if hasattr(exc, 'missing_keys'): missing_keys.extend(exc.missing) if hasattr(exc, 'from_key'): # FIXME: We assume signature failures happen because # the key is locked. Are there any other reasons? locked_keys.append(exc.from_key) for ev in events: ev.flags = Event.COMPLETE ev.message = message config.event_log.log_event(ev) self._ignore_exception() # FIXME: Also fatal, when the SMTP server REJECTS the mail except:
def _create_from_to_cc(cls, idx, session, trees): config = session.config ahp = AddressHeaderParser() ref_from, ref_to, ref_cc = [], [], [] result = {'from': '', 'to': [], 'cc': []} def merge_contact(ai): vcard = session.config.vcards.get_vcard(ai.address) if vcard: ai.merge_vcard(vcard) return ai # Parse the headers, so we know what we're working with. We prune # some of the duplicates at this stage. for addrs in [t['addresses'] for t in trees]: alist = [] for dst, addresses in ((ref_from, addrs.get('reply-to') or addrs.get('from', [])), (ref_to, addrs.get('to', [])), (ref_cc, addrs.get('cc', []))): alist += [d.address for d in dst] dst.extend([a for a in addresses if a.address not in alist]) # 1st, choose a from address. We'll use the system default if # nothing is found, but hopefully we'll find an address we # recognize in one of the headers. from_address = (session.config.prefs.default_email or session.config.profiles[0].email) profile_emails = [p.email for p in session.config.profiles if p.email] for src in (ref_from, ref_to, ref_cc): matches = [s for s in src if s.address in profile_emails] if matches: from_address = matches[0].address break result['from'] = ahp.normalized(addresses=[ AddressInfo(p.email, p.name) for p in session.config.profiles if p.email == from_address ], force_name=True) def addresses(addrs, exclude=[]): alist = [from_address] + [a.address for a in exclude] return ahp.normalized_addresses(addresses=[ merge_contact(a) for a in addrs if a.address not in alist and not a.address.startswith( 'noreply@') and '@noreply' not in a.address ], with_keys=True, force_name=True) # If only replying to messages sent from chosen from, then this is # a follow-up or clarification, so just use the same headers. if len([e for e in ref_from if e.address == from_address]) == len(ref_from): if ref_to: result['to'] = addresses(ref_to) if ref_cc: result['cc'] = addresses(ref_cc) # Else, if replying to other people: # - Construct To from the From lines, excluding own from # - Construct Cc from the To and CC lines, except new To/From else: result['to'] = addresses(ref_from) result['cc'] = addresses(ref_to + ref_cc, exclude=ref_from) return result