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 extract_autocrypt_header(msg, to=None, optional_attrs=None): # Autocrypt requires there only be one From header froms = msg.get_all("From") or [] if len(froms) != 1: return {} # Extract the from address for comparisons below. We compare the # canonicalized versions, which is not the strictest interpretation # of the spec, but feels like a reasonable balance here. from mailpile.mailutils.addresses import AddressHeaderParser from_addrs = AddressHeaderParser(froms[0]) if len(from_addrs) != 1: return {} from_addr = canonicalize_email(from_addrs[0].address) to = canonicalize_email(to) if to else None all_results = [] for inb in (msg.get_all("Autocrypt") or []): res = parse_autocrypt_headervalue(inb, optional_attrs=optional_attrs) if res: if ((not to or canonicalize_email(res['addr']) == to) and (canonicalize_email(res['addr']) == from_addr)): all_results.append(res) # Return parsed header iff we found exactly one. if len(all_results) == 1: return all_results[0] else: return {}
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.addresses 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.addresses 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): problem, tree = None, {} try: # We load the message in stages (relying on the internal cache # to make this not slow), so we can report more accurately what # has failed. problem = _('Failed to load and parse message data.') msg = email.get_msg(pgpmime=False) problem = _('Failed process message crypto (decrypt, etc).') msg = email.get_msg(pgpmime='default') problem = _('Failed to parse message.') tree = email.get_message_tree(want=(email.WANT_MSG_TREE_PGP + self.WANT_MSG_TREE)) problem = _('Failed process message crypto (decrypt, etc).') email.evaluate_pgp(tree, decrypt=True) problem = _("Failed to evalute sender trust") evaluate_sender_trust(self.session.config, email, tree) editing_strings = tree.get('editing_strings') if editing_strings: for key in ('from', 'to', 'cc', 'bcc'): problem = _("Failed to parse %s headers." % key) 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) problem = None except Exception as e: if problem: problem += ' ' + _('Message may be corrupt!') details = { 'error': unicode(e), 'details': problem, 'traceback': traceback.format_exc(e) } details.update(self._troubleshoot_missing_message(email, tree)) self['errors'] = self.get('errors', {}) self['errors'][email.msg_mid()] = details return self._prune_msg_tree(tree)
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._get_sender_profile(sender, kwargs) if profile['vcard'] is not None: sender_keyid = profile['vcard'].pgp_key crypto_format = profile.get('crypto_format') or '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
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: