class MailpileMailbox(UnorderedPicklable(mailbox.Maildir, editable=True)): """A Maildir class that supports pickling and a few mailpile specifics.""" supported_platform = None @classmethod def parse_path(cls, config, fn, create=False): if (((cls.supported_platform is None) or (cls.supported_platform == sys.platform[:3].lower())) and ((os.path.isdir(fn) and os.path.exists(os.path.join(fn, 'cur'))) or (create and not os.path.exists(fn)))): return (fn, ) raise ValueError('Not a Maildir: %s' % fn) def _refresh(self): with self._lock: mailbox.Maildir._refresh(self) # Dotfiles are not mail. Ignore them. for t in [k for k in self._toc.keys() if k.startswith('.')]: del self._toc[t] def __unicode__(self): return _("Maildir at %s") % self._path def _describe_msg_by_ptr(self, msg_ptr): return _("e-mail in file %s") % self._lookup(msg_ptr[MBX_ID_LEN:]) def get_metadata_keywords(self, toc_id): subdir, name = os.path.split(self._lookup(toc_id)) if self.colon in name: flags = name.split(self.colon)[-1] if flags[:2] == '2,': return ['%s:maildir' % c for c in flags[2:]] return []
class MailpileMailbox(UnorderedPicklable(IMAPMailbox)): @classmethod def parse_path(cls, path, create=False): if path.startswith("imap://"): url = path[7:] try: serverpart, mailbox = url.split("/") except ValueError: serverpart = url mailbox = None userpart, server = serverpart.split("@") user, password = userpart.split(":") # WARNING: Order must match IMAPMailbox.__init__(...) return (server, 993, user, password) raise ValueError('Not an IMAP url: %s' % path) def __getstate__(self): odict = self.__dict__.copy() # Pickle can't handle file and function objects. del odict['_mailbox'] del odict['_save_to'] return odict def get_msg_size(self, toc_id): # FIXME: We should make this less horrible. fd = self.get_file(toc_id) fd.seek(0, 2) return fd.tell()
class MailpileMailbox(UnorderedPicklable(MacMaildir)): """A Mac Mail.app maildir class that supports pickling etc.""" @classmethod def parse_path(cls, config, fn, create=False): if (os.path.isdir(fn) and os.path.exists(os.path.join(fn, 'Info.plist'))): return (fn, ) raise ValueError('Not a Mac Mail.app Maildir: %s' % fn)
class MailpileMailbox(UnorderedPicklable(mailbox.Maildir, editable=True)): """A Maildir class that supports pickling and a few mailpile specifics.""" supported_platform = None @classmethod def parse_path(cls, fn, create=False): if (((cls.supported_platform is None) or (cls.supported_platform in system().lower())) and ((os.path.isdir(fn) and os.path.exists(os.path.join(fn, 'cur'))) or (create and not os.path.exists(fn)))): return (fn, ) raise ValueError('Not a Maildir: %s' % fn)
class MailpileMailbox(UnorderedPicklable(MacMaildir)): """A Mac Mail.app maildir class that supports pickling etc.""" @classmethod def parse_path(cls, config, fn, create=False, allow_empty=False): if (os.path.isdir(fn) and os.path.exists(os.path.join(fn, 'Info.plist'))): return (fn, ) raise ValueError('Not a Mac Mail.app Maildir: %s' % fn) def __unicode__(self): return _("Mac Maildir %s") % self._mailroot def _describe_msg_by_ptr(self, msg_ptr): return _("e-mail in file %s") % self._lookup(msg_ptr[MBX_ID_LEN:])
class MailpileMailbox(UnorderedPicklable(mailbox.Maildir, editable=True)): """A Maildir class that supports pickling and a few mailpile specifics.""" supported_platform = None @classmethod def parse_path(cls, config, fn, create=False): if (((cls.supported_platform is None) or (cls.supported_platform in system().lower())) and ((os.path.isdir(fn) and os.path.exists(os.path.join(fn, 'cur'))) or (create and not os.path.exists(fn)))): return (fn, ) raise ValueError('Not a Maildir: %s' % fn) def _refresh(self): mailbox.Maildir._refresh(self) # Dotfiles are not mail. Ignore them. for t in [k for k in self._toc.keys() if k.startswith('.')]: del self._toc[t]
class MailpileMailbox(UnorderedPicklable(IMAPMailbox)): UNPICKLABLE = ['_mailbox'] @classmethod def parse_path(cls, config, path, create=False): if path.startswith("imap://"): url = path[7:] try: serverpart, mailbox = url.split("/") except ValueError: serverpart = url mailbox = None userpart, server = serverpart.split("@") user, password = userpart.split(":") # WARNING: Order must match IMAPMailbox.__init__(...) return (server, 993, user, password) raise ValueError('Not an IMAP url: %s' % path) def get_msg_size(self, toc_id): # FIXME: We should make this less horrible. fd = self.get_file(toc_id) fd.seek(0, 2) return fd.tell()
class MailpileMailbox(UnorderedPicklable(POP3Mailbox)): UNPICKLABLE = ['_pop3', '_debug'] @classmethod def parse_path(cls, config, path, create=False): path = path.split('/') if path and path[0].lower() in ('pop:', 'pop3:', 'pop3_ssl:', 'pop3s:'): proto = path[0][:-1].lower() userpart, server = path[2].rsplit("@", 1) user, password = userpart.rsplit(":", 1) if ":" in server: server, port = server.split(":", 1) else: port = 995 if ('s' in proto) else 110 # This is a hack for GMail if 'recent' in path[3:]: user = '******' + user if not config: debug = False elif 'pop3' in config.sys.debug: debug = 99 elif 'rescan' in config.sys.debug: debug = 1 else: debug = False # WARNING: Order must match POP3Mailbox.__init__(...) return (server, user, password, 's' in proto, int(port), debug) raise ValueError('Not a POP3 url: %s' % path) def save(self, *args, **kwargs): # Do not save state locally pass
class MailpileMailbox(UnorderedPicklable(mailbox.Maildir, editable=True)): """A Maildir class that supports pickling and a few mailpile specifics.""" supported_platform = None @classmethod def parse_path(cls, config, fn, create=False): if (((cls.supported_platform is None) or (cls.supported_platform == sys.platform[:3].lower())) and ((os.path.isdir(fn) and os.path.exists(os.path.join(fn, 'cur')) and os.path.exists(os.path.join(fn, 'wervd.ver'))) or (create and not os.path.exists(fn)))): return (fn, ) raise ValueError('Not a Maildir: %s' % fn) def __init2__(self, *args, **kwargs): open(os.path.join(self._path, 'wervd.ver'), 'w+b').write('0') def remove(self, key): # FIXME: Remove all the copies of this message! fn = self._lookup(key) del self._toc[key] safe_remove(fn) def _refresh(self): mailbox.Maildir._refresh(self) # WERVD mail names don't have dots in them for t in [k for k in self._toc.keys() if '.' in k]: del self._toc[t] safe_remove() # Try to remove any postponed removals def _get_fd(self, key): fd = open(os.path.join(self._path, self._lookup(key)), 'rb') key = self._decryption_key_func() if key: fd = DecryptingStreamer(fd, mep_key=key, name='WERVD') return fd def get_message(self, key): """Return a Message representation or raise a KeyError.""" with self._get_fd(key) as fd: if self._factory: return self._factory(fd) else: return mailbox.MaildirMessage(fd) def get_string(self, key): with self._get_fd(key) as fd: return fd.read() def get_file(self, key): return StringIO.StringIO(self.get_string(key)) def add(self, message, copies=1): """Add message and return assigned key.""" key = self._encryption_key_func() es = None try: tmpdir = os.path.join(self._path, 'tmp') if key: es = EncryptingStreamer(key, dir=tmpdir, name='WERVD', delimited=False) else: es = ChecksummingStreamer(dir=tmpdir, name='WERVD') self._dump_message(message, es) es.finish() # We are using the MD5 to detect file system corruption, not in a # security context - so using as little as 40 bits should be fine. saved = False key = None for l in range(10, len(es.outer_md5sum)): key = es.outer_md5sum[:l] fn = os.path.join(self._path, 'new', key) if not os.path.exists(fn): es.save(fn) saved = self._toc[key] = os.path.join('new', key) break if not saved: raise mailbox.ExternalClashError( _('Could not find a filename ' 'for the message.')) for cpn in range(1, copies): fn = os.path.join(self._path, 'new', '%s.%s' % (key, cpn)) with mailbox._create_carefully(fn) as ofd: es.save_copy(ofd) return key finally: if es is not None: es.close() def _dump_message(self, message, target): if isinstance(message, email.message.Message): gen = email.generator.Generator(target, False, 0) gen.flatten(message) elif isinstance(message, str): target.write(message) else: raise TypeError(_('Invalid message type: %s') % type(message)) def __setitem__(self, key, message): raise IOError(_('Mailbox messages are immutable'))
class MailpileMailbox(UnorderedPicklable(mailbox.Maildir, editable=True)): """A Maildir class that supports pickling and a few mailpile specifics.""" supported_platform = None colon = '!' # Works on both Windows and Unix # FIXME: Copies were part of the original WERVD spec, to compensate for # the additional fragility of encrypted data. This hasn't been # implemented however, and while SSDs are expensive it is not # obvious that doubling (or tripling...) the storage requirements # for all e-mail is a cost folks are willing to pay for never # losing a message to the occaisional bitflips. So this is all # commented out at the moment. Revisit? # MAX_COPIES = 5 @classmethod def parse_path(cls, config, fn, create=False, allow_empty=False): if (((cls.supported_platform is None) or (cls.supported_platform == sys.platform[:3].lower())) and ((os.path.isdir(fn) and os.path.exists(os.path.join(fn, 'cur')) and os.path.exists(os.path.join(fn, 'wervd.ver'))) or (create and not os.path.exists(fn)))): return (fn, ) raise ValueError('Not a Mailpile Maildir: %s' % fn) def __init2__(self, *args, **kwargs): open(os.path.join(self._path, 'wervd.ver'), 'w+b').write('0') def __unicode__(self): return _("Mailpile mailbox at %s") % self._path def _describe_msg_by_ptr(self, msg_ptr): return _("e-mail in file %s") % self._lookup(msg_ptr[MBX_ID_LEN:]) # FIXME: Copies # def _copy_paths(self, where, key, copies): # for cpn in range(1, copies): # yield os.path.join(self._path, where, '%s.%s' % (key, cpn)) def remove(self, key): with self._lock: fn = os.path.join(self._path, self._lookup(key)) del self._toc[key] safe_remove(fn) # FIXME: Copies # # Also remove all the copies of this message! # key = os.path.basename(fn) # for where in ('cur', 'new', 'tmp'): # for copy_fn in self._copy_paths(where, key, self.MAX_COPIES): # if os.path.exists(copy_fn): # safe_remove(copy_fn) # else: # break def _refresh(self): with self._lock: mailbox.Maildir._refresh(self) # WERVD mail names don't have dots in them for t in [k for k in self._toc.keys() if '.' in k]: del self._toc[t] safe_remove() # Try to remove any postponed removals def _get_fd(self, key): with self._lock: fn = os.path.join(self._path, self._lookup(key)) mep_key = self._decryption_key_func() fd = open(fn, 'rb') if mep_key: fd = DecryptingStreamer(fd, mep_key=mep_key, name='WERVD(%s)' % fn) return fd def get_message(self, key): """Return a Message representation or raise a KeyError.""" with self._lock: with self._get_fd(key) as fd: if self._factory: return self._factory(fd) else: return mailbox.MaildirMessage(fd) def get_string(self, key): with self._lock: with self._get_fd(key) as fd: return fd.read() def get_file(self, key): with self._lock: return StringIO.StringIO(self.get_string(key)) def get_metadata_keywords(self, toc_id): subdir, name = os.path.split(self._lookup(toc_id)) if self.colon in name: flags = name.split(self.colon)[-1] if flags[:2] == '2,': return ['%s:maildir' % c for c in flags[2:]] return [] def set_metadata_keywords(self, toc_id, kws): with self._lock: old_fpath = self._lookup(toc_id) new_fpath = old_fpath.rsplit(self.colon, 1)[0] flags = ''.join(sorted([k[0] for k in kws])) if flags: new_fpath += '%s2,%s' % (self.colon, flags) if new_fpath != old_fpath: os.rename(os.path.join(self._path, old_fpath), os.path.join(self._path, new_fpath)) self._toc[toc_id] = new_fpath def add(self, message): """Add message and return assigned key.""" key = self._encryption_key_func() es = None try: tmpdir = os.path.join(self._path, 'tmp') if not os.path.exists(tmpdir): os.mkdir(tmpdir, 0o700) if key: es = EncryptingStreamer(key, dir=tmpdir, name='WERVD', delimited=False) else: es = ChecksummingStreamer(dir=tmpdir, name='WERVD') self._dump_message(message, es) es.finish() # We are using the MAC to detect file system corruption, not in a # security context - so using as little as 40 bits should be fine. saved = False key = None outer_mac = es.outer_mac_sha256() for l in range(10, len(outer_mac)): key = outer_mac[:l] fn = os.path.join(self._path, 'new', key) if not os.path.exists(fn): es.save(fn) saved = self._toc[key] = os.path.join('new', key) break if not saved: raise mailbox.ExternalClashError( _('Could not find a filename ' 'for the message.')) # FIXME: Copies # for fn in self._copy_paths('new', key, copies): # with mailbox._create_carefully(fn) as ofd: # es.save_copy(ofd) return key finally: if es is not None: es.close() def _dump_message(self, message, target): if isinstance(message, email.message.Message): gen = email.generator.Generator(target, False, 0) gen.flatten(message) elif isinstance(message, str): target.write(message) else: raise TypeError(_('Invalid message type: %s') % type(message)) def __setitem__(self, key, message): raise IOError(_('Mailbox messages are immutable'))