1*cda5da8dSAndroid Build Coastguard Worker"""Read/write support for Maildir, mbox, MH, Babyl, and MMDF mailboxes.""" 2*cda5da8dSAndroid Build Coastguard Worker 3*cda5da8dSAndroid Build Coastguard Worker# Notes for authors of new mailbox subclasses: 4*cda5da8dSAndroid Build Coastguard Worker# 5*cda5da8dSAndroid Build Coastguard Worker# Remember to fsync() changes to disk before closing a modified file 6*cda5da8dSAndroid Build Coastguard Worker# or returning from a flush() method. See functions _sync_flush() and 7*cda5da8dSAndroid Build Coastguard Worker# _sync_close(). 8*cda5da8dSAndroid Build Coastguard Worker 9*cda5da8dSAndroid Build Coastguard Workerimport os 10*cda5da8dSAndroid Build Coastguard Workerimport time 11*cda5da8dSAndroid Build Coastguard Workerimport calendar 12*cda5da8dSAndroid Build Coastguard Workerimport socket 13*cda5da8dSAndroid Build Coastguard Workerimport errno 14*cda5da8dSAndroid Build Coastguard Workerimport copy 15*cda5da8dSAndroid Build Coastguard Workerimport warnings 16*cda5da8dSAndroid Build Coastguard Workerimport email 17*cda5da8dSAndroid Build Coastguard Workerimport email.message 18*cda5da8dSAndroid Build Coastguard Workerimport email.generator 19*cda5da8dSAndroid Build Coastguard Workerimport io 20*cda5da8dSAndroid Build Coastguard Workerimport contextlib 21*cda5da8dSAndroid Build Coastguard Workerfrom types import GenericAlias 22*cda5da8dSAndroid Build Coastguard Workertry: 23*cda5da8dSAndroid Build Coastguard Worker import fcntl 24*cda5da8dSAndroid Build Coastguard Workerexcept ImportError: 25*cda5da8dSAndroid Build Coastguard Worker fcntl = None 26*cda5da8dSAndroid Build Coastguard Worker 27*cda5da8dSAndroid Build Coastguard Worker__all__ = ['Mailbox', 'Maildir', 'mbox', 'MH', 'Babyl', 'MMDF', 28*cda5da8dSAndroid Build Coastguard Worker 'Message', 'MaildirMessage', 'mboxMessage', 'MHMessage', 29*cda5da8dSAndroid Build Coastguard Worker 'BabylMessage', 'MMDFMessage', 'Error', 'NoSuchMailboxError', 30*cda5da8dSAndroid Build Coastguard Worker 'NotEmptyError', 'ExternalClashError', 'FormatError'] 31*cda5da8dSAndroid Build Coastguard Worker 32*cda5da8dSAndroid Build Coastguard Workerlinesep = os.linesep.encode('ascii') 33*cda5da8dSAndroid Build Coastguard Worker 34*cda5da8dSAndroid Build Coastguard Workerclass Mailbox: 35*cda5da8dSAndroid Build Coastguard Worker """A group of messages in a particular place.""" 36*cda5da8dSAndroid Build Coastguard Worker 37*cda5da8dSAndroid Build Coastguard Worker def __init__(self, path, factory=None, create=True): 38*cda5da8dSAndroid Build Coastguard Worker """Initialize a Mailbox instance.""" 39*cda5da8dSAndroid Build Coastguard Worker self._path = os.path.abspath(os.path.expanduser(path)) 40*cda5da8dSAndroid Build Coastguard Worker self._factory = factory 41*cda5da8dSAndroid Build Coastguard Worker 42*cda5da8dSAndroid Build Coastguard Worker def add(self, message): 43*cda5da8dSAndroid Build Coastguard Worker """Add message and return assigned key.""" 44*cda5da8dSAndroid Build Coastguard Worker raise NotImplementedError('Method must be implemented by subclass') 45*cda5da8dSAndroid Build Coastguard Worker 46*cda5da8dSAndroid Build Coastguard Worker def remove(self, key): 47*cda5da8dSAndroid Build Coastguard Worker """Remove the keyed message; raise KeyError if it doesn't exist.""" 48*cda5da8dSAndroid Build Coastguard Worker raise NotImplementedError('Method must be implemented by subclass') 49*cda5da8dSAndroid Build Coastguard Worker 50*cda5da8dSAndroid Build Coastguard Worker def __delitem__(self, key): 51*cda5da8dSAndroid Build Coastguard Worker self.remove(key) 52*cda5da8dSAndroid Build Coastguard Worker 53*cda5da8dSAndroid Build Coastguard Worker def discard(self, key): 54*cda5da8dSAndroid Build Coastguard Worker """If the keyed message exists, remove it.""" 55*cda5da8dSAndroid Build Coastguard Worker try: 56*cda5da8dSAndroid Build Coastguard Worker self.remove(key) 57*cda5da8dSAndroid Build Coastguard Worker except KeyError: 58*cda5da8dSAndroid Build Coastguard Worker pass 59*cda5da8dSAndroid Build Coastguard Worker 60*cda5da8dSAndroid Build Coastguard Worker def __setitem__(self, key, message): 61*cda5da8dSAndroid Build Coastguard Worker """Replace the keyed message; raise KeyError if it doesn't exist.""" 62*cda5da8dSAndroid Build Coastguard Worker raise NotImplementedError('Method must be implemented by subclass') 63*cda5da8dSAndroid Build Coastguard Worker 64*cda5da8dSAndroid Build Coastguard Worker def get(self, key, default=None): 65*cda5da8dSAndroid Build Coastguard Worker """Return the keyed message, or default if it doesn't exist.""" 66*cda5da8dSAndroid Build Coastguard Worker try: 67*cda5da8dSAndroid Build Coastguard Worker return self.__getitem__(key) 68*cda5da8dSAndroid Build Coastguard Worker except KeyError: 69*cda5da8dSAndroid Build Coastguard Worker return default 70*cda5da8dSAndroid Build Coastguard Worker 71*cda5da8dSAndroid Build Coastguard Worker def __getitem__(self, key): 72*cda5da8dSAndroid Build Coastguard Worker """Return the keyed message; raise KeyError if it doesn't exist.""" 73*cda5da8dSAndroid Build Coastguard Worker if not self._factory: 74*cda5da8dSAndroid Build Coastguard Worker return self.get_message(key) 75*cda5da8dSAndroid Build Coastguard Worker else: 76*cda5da8dSAndroid Build Coastguard Worker with contextlib.closing(self.get_file(key)) as file: 77*cda5da8dSAndroid Build Coastguard Worker return self._factory(file) 78*cda5da8dSAndroid Build Coastguard Worker 79*cda5da8dSAndroid Build Coastguard Worker def get_message(self, key): 80*cda5da8dSAndroid Build Coastguard Worker """Return a Message representation or raise a KeyError.""" 81*cda5da8dSAndroid Build Coastguard Worker raise NotImplementedError('Method must be implemented by subclass') 82*cda5da8dSAndroid Build Coastguard Worker 83*cda5da8dSAndroid Build Coastguard Worker def get_string(self, key): 84*cda5da8dSAndroid Build Coastguard Worker """Return a string representation or raise a KeyError. 85*cda5da8dSAndroid Build Coastguard Worker 86*cda5da8dSAndroid Build Coastguard Worker Uses email.message.Message to create a 7bit clean string 87*cda5da8dSAndroid Build Coastguard Worker representation of the message.""" 88*cda5da8dSAndroid Build Coastguard Worker return email.message_from_bytes(self.get_bytes(key)).as_string() 89*cda5da8dSAndroid Build Coastguard Worker 90*cda5da8dSAndroid Build Coastguard Worker def get_bytes(self, key): 91*cda5da8dSAndroid Build Coastguard Worker """Return a byte string representation or raise a KeyError.""" 92*cda5da8dSAndroid Build Coastguard Worker raise NotImplementedError('Method must be implemented by subclass') 93*cda5da8dSAndroid Build Coastguard Worker 94*cda5da8dSAndroid Build Coastguard Worker def get_file(self, key): 95*cda5da8dSAndroid Build Coastguard Worker """Return a file-like representation or raise a KeyError.""" 96*cda5da8dSAndroid Build Coastguard Worker raise NotImplementedError('Method must be implemented by subclass') 97*cda5da8dSAndroid Build Coastguard Worker 98*cda5da8dSAndroid Build Coastguard Worker def iterkeys(self): 99*cda5da8dSAndroid Build Coastguard Worker """Return an iterator over keys.""" 100*cda5da8dSAndroid Build Coastguard Worker raise NotImplementedError('Method must be implemented by subclass') 101*cda5da8dSAndroid Build Coastguard Worker 102*cda5da8dSAndroid Build Coastguard Worker def keys(self): 103*cda5da8dSAndroid Build Coastguard Worker """Return a list of keys.""" 104*cda5da8dSAndroid Build Coastguard Worker return list(self.iterkeys()) 105*cda5da8dSAndroid Build Coastguard Worker 106*cda5da8dSAndroid Build Coastguard Worker def itervalues(self): 107*cda5da8dSAndroid Build Coastguard Worker """Return an iterator over all messages.""" 108*cda5da8dSAndroid Build Coastguard Worker for key in self.iterkeys(): 109*cda5da8dSAndroid Build Coastguard Worker try: 110*cda5da8dSAndroid Build Coastguard Worker value = self[key] 111*cda5da8dSAndroid Build Coastguard Worker except KeyError: 112*cda5da8dSAndroid Build Coastguard Worker continue 113*cda5da8dSAndroid Build Coastguard Worker yield value 114*cda5da8dSAndroid Build Coastguard Worker 115*cda5da8dSAndroid Build Coastguard Worker def __iter__(self): 116*cda5da8dSAndroid Build Coastguard Worker return self.itervalues() 117*cda5da8dSAndroid Build Coastguard Worker 118*cda5da8dSAndroid Build Coastguard Worker def values(self): 119*cda5da8dSAndroid Build Coastguard Worker """Return a list of messages. Memory intensive.""" 120*cda5da8dSAndroid Build Coastguard Worker return list(self.itervalues()) 121*cda5da8dSAndroid Build Coastguard Worker 122*cda5da8dSAndroid Build Coastguard Worker def iteritems(self): 123*cda5da8dSAndroid Build Coastguard Worker """Return an iterator over (key, message) tuples.""" 124*cda5da8dSAndroid Build Coastguard Worker for key in self.iterkeys(): 125*cda5da8dSAndroid Build Coastguard Worker try: 126*cda5da8dSAndroid Build Coastguard Worker value = self[key] 127*cda5da8dSAndroid Build Coastguard Worker except KeyError: 128*cda5da8dSAndroid Build Coastguard Worker continue 129*cda5da8dSAndroid Build Coastguard Worker yield (key, value) 130*cda5da8dSAndroid Build Coastguard Worker 131*cda5da8dSAndroid Build Coastguard Worker def items(self): 132*cda5da8dSAndroid Build Coastguard Worker """Return a list of (key, message) tuples. Memory intensive.""" 133*cda5da8dSAndroid Build Coastguard Worker return list(self.iteritems()) 134*cda5da8dSAndroid Build Coastguard Worker 135*cda5da8dSAndroid Build Coastguard Worker def __contains__(self, key): 136*cda5da8dSAndroid Build Coastguard Worker """Return True if the keyed message exists, False otherwise.""" 137*cda5da8dSAndroid Build Coastguard Worker raise NotImplementedError('Method must be implemented by subclass') 138*cda5da8dSAndroid Build Coastguard Worker 139*cda5da8dSAndroid Build Coastguard Worker def __len__(self): 140*cda5da8dSAndroid Build Coastguard Worker """Return a count of messages in the mailbox.""" 141*cda5da8dSAndroid Build Coastguard Worker raise NotImplementedError('Method must be implemented by subclass') 142*cda5da8dSAndroid Build Coastguard Worker 143*cda5da8dSAndroid Build Coastguard Worker def clear(self): 144*cda5da8dSAndroid Build Coastguard Worker """Delete all messages.""" 145*cda5da8dSAndroid Build Coastguard Worker for key in self.keys(): 146*cda5da8dSAndroid Build Coastguard Worker self.discard(key) 147*cda5da8dSAndroid Build Coastguard Worker 148*cda5da8dSAndroid Build Coastguard Worker def pop(self, key, default=None): 149*cda5da8dSAndroid Build Coastguard Worker """Delete the keyed message and return it, or default.""" 150*cda5da8dSAndroid Build Coastguard Worker try: 151*cda5da8dSAndroid Build Coastguard Worker result = self[key] 152*cda5da8dSAndroid Build Coastguard Worker except KeyError: 153*cda5da8dSAndroid Build Coastguard Worker return default 154*cda5da8dSAndroid Build Coastguard Worker self.discard(key) 155*cda5da8dSAndroid Build Coastguard Worker return result 156*cda5da8dSAndroid Build Coastguard Worker 157*cda5da8dSAndroid Build Coastguard Worker def popitem(self): 158*cda5da8dSAndroid Build Coastguard Worker """Delete an arbitrary (key, message) pair and return it.""" 159*cda5da8dSAndroid Build Coastguard Worker for key in self.iterkeys(): 160*cda5da8dSAndroid Build Coastguard Worker return (key, self.pop(key)) # This is only run once. 161*cda5da8dSAndroid Build Coastguard Worker else: 162*cda5da8dSAndroid Build Coastguard Worker raise KeyError('No messages in mailbox') 163*cda5da8dSAndroid Build Coastguard Worker 164*cda5da8dSAndroid Build Coastguard Worker def update(self, arg=None): 165*cda5da8dSAndroid Build Coastguard Worker """Change the messages that correspond to certain keys.""" 166*cda5da8dSAndroid Build Coastguard Worker if hasattr(arg, 'iteritems'): 167*cda5da8dSAndroid Build Coastguard Worker source = arg.iteritems() 168*cda5da8dSAndroid Build Coastguard Worker elif hasattr(arg, 'items'): 169*cda5da8dSAndroid Build Coastguard Worker source = arg.items() 170*cda5da8dSAndroid Build Coastguard Worker else: 171*cda5da8dSAndroid Build Coastguard Worker source = arg 172*cda5da8dSAndroid Build Coastguard Worker bad_key = False 173*cda5da8dSAndroid Build Coastguard Worker for key, message in source: 174*cda5da8dSAndroid Build Coastguard Worker try: 175*cda5da8dSAndroid Build Coastguard Worker self[key] = message 176*cda5da8dSAndroid Build Coastguard Worker except KeyError: 177*cda5da8dSAndroid Build Coastguard Worker bad_key = True 178*cda5da8dSAndroid Build Coastguard Worker if bad_key: 179*cda5da8dSAndroid Build Coastguard Worker raise KeyError('No message with key(s)') 180*cda5da8dSAndroid Build Coastguard Worker 181*cda5da8dSAndroid Build Coastguard Worker def flush(self): 182*cda5da8dSAndroid Build Coastguard Worker """Write any pending changes to the disk.""" 183*cda5da8dSAndroid Build Coastguard Worker raise NotImplementedError('Method must be implemented by subclass') 184*cda5da8dSAndroid Build Coastguard Worker 185*cda5da8dSAndroid Build Coastguard Worker def lock(self): 186*cda5da8dSAndroid Build Coastguard Worker """Lock the mailbox.""" 187*cda5da8dSAndroid Build Coastguard Worker raise NotImplementedError('Method must be implemented by subclass') 188*cda5da8dSAndroid Build Coastguard Worker 189*cda5da8dSAndroid Build Coastguard Worker def unlock(self): 190*cda5da8dSAndroid Build Coastguard Worker """Unlock the mailbox if it is locked.""" 191*cda5da8dSAndroid Build Coastguard Worker raise NotImplementedError('Method must be implemented by subclass') 192*cda5da8dSAndroid Build Coastguard Worker 193*cda5da8dSAndroid Build Coastguard Worker def close(self): 194*cda5da8dSAndroid Build Coastguard Worker """Flush and close the mailbox.""" 195*cda5da8dSAndroid Build Coastguard Worker raise NotImplementedError('Method must be implemented by subclass') 196*cda5da8dSAndroid Build Coastguard Worker 197*cda5da8dSAndroid Build Coastguard Worker def _string_to_bytes(self, message): 198*cda5da8dSAndroid Build Coastguard Worker # If a message is not 7bit clean, we refuse to handle it since it 199*cda5da8dSAndroid Build Coastguard Worker # likely came from reading invalid messages in text mode, and that way 200*cda5da8dSAndroid Build Coastguard Worker # lies mojibake. 201*cda5da8dSAndroid Build Coastguard Worker try: 202*cda5da8dSAndroid Build Coastguard Worker return message.encode('ascii') 203*cda5da8dSAndroid Build Coastguard Worker except UnicodeError: 204*cda5da8dSAndroid Build Coastguard Worker raise ValueError("String input must be ASCII-only; " 205*cda5da8dSAndroid Build Coastguard Worker "use bytes or a Message instead") 206*cda5da8dSAndroid Build Coastguard Worker 207*cda5da8dSAndroid Build Coastguard Worker # Whether each message must end in a newline 208*cda5da8dSAndroid Build Coastguard Worker _append_newline = False 209*cda5da8dSAndroid Build Coastguard Worker 210*cda5da8dSAndroid Build Coastguard Worker def _dump_message(self, message, target, mangle_from_=False): 211*cda5da8dSAndroid Build Coastguard Worker # This assumes the target file is open in binary mode. 212*cda5da8dSAndroid Build Coastguard Worker """Dump message contents to target file.""" 213*cda5da8dSAndroid Build Coastguard Worker if isinstance(message, email.message.Message): 214*cda5da8dSAndroid Build Coastguard Worker buffer = io.BytesIO() 215*cda5da8dSAndroid Build Coastguard Worker gen = email.generator.BytesGenerator(buffer, mangle_from_, 0) 216*cda5da8dSAndroid Build Coastguard Worker gen.flatten(message) 217*cda5da8dSAndroid Build Coastguard Worker buffer.seek(0) 218*cda5da8dSAndroid Build Coastguard Worker data = buffer.read() 219*cda5da8dSAndroid Build Coastguard Worker data = data.replace(b'\n', linesep) 220*cda5da8dSAndroid Build Coastguard Worker target.write(data) 221*cda5da8dSAndroid Build Coastguard Worker if self._append_newline and not data.endswith(linesep): 222*cda5da8dSAndroid Build Coastguard Worker # Make sure the message ends with a newline 223*cda5da8dSAndroid Build Coastguard Worker target.write(linesep) 224*cda5da8dSAndroid Build Coastguard Worker elif isinstance(message, (str, bytes, io.StringIO)): 225*cda5da8dSAndroid Build Coastguard Worker if isinstance(message, io.StringIO): 226*cda5da8dSAndroid Build Coastguard Worker warnings.warn("Use of StringIO input is deprecated, " 227*cda5da8dSAndroid Build Coastguard Worker "use BytesIO instead", DeprecationWarning, 3) 228*cda5da8dSAndroid Build Coastguard Worker message = message.getvalue() 229*cda5da8dSAndroid Build Coastguard Worker if isinstance(message, str): 230*cda5da8dSAndroid Build Coastguard Worker message = self._string_to_bytes(message) 231*cda5da8dSAndroid Build Coastguard Worker if mangle_from_: 232*cda5da8dSAndroid Build Coastguard Worker message = message.replace(b'\nFrom ', b'\n>From ') 233*cda5da8dSAndroid Build Coastguard Worker message = message.replace(b'\n', linesep) 234*cda5da8dSAndroid Build Coastguard Worker target.write(message) 235*cda5da8dSAndroid Build Coastguard Worker if self._append_newline and not message.endswith(linesep): 236*cda5da8dSAndroid Build Coastguard Worker # Make sure the message ends with a newline 237*cda5da8dSAndroid Build Coastguard Worker target.write(linesep) 238*cda5da8dSAndroid Build Coastguard Worker elif hasattr(message, 'read'): 239*cda5da8dSAndroid Build Coastguard Worker if hasattr(message, 'buffer'): 240*cda5da8dSAndroid Build Coastguard Worker warnings.warn("Use of text mode files is deprecated, " 241*cda5da8dSAndroid Build Coastguard Worker "use a binary mode file instead", DeprecationWarning, 3) 242*cda5da8dSAndroid Build Coastguard Worker message = message.buffer 243*cda5da8dSAndroid Build Coastguard Worker lastline = None 244*cda5da8dSAndroid Build Coastguard Worker while True: 245*cda5da8dSAndroid Build Coastguard Worker line = message.readline() 246*cda5da8dSAndroid Build Coastguard Worker # Universal newline support. 247*cda5da8dSAndroid Build Coastguard Worker if line.endswith(b'\r\n'): 248*cda5da8dSAndroid Build Coastguard Worker line = line[:-2] + b'\n' 249*cda5da8dSAndroid Build Coastguard Worker elif line.endswith(b'\r'): 250*cda5da8dSAndroid Build Coastguard Worker line = line[:-1] + b'\n' 251*cda5da8dSAndroid Build Coastguard Worker if not line: 252*cda5da8dSAndroid Build Coastguard Worker break 253*cda5da8dSAndroid Build Coastguard Worker if mangle_from_ and line.startswith(b'From '): 254*cda5da8dSAndroid Build Coastguard Worker line = b'>From ' + line[5:] 255*cda5da8dSAndroid Build Coastguard Worker line = line.replace(b'\n', linesep) 256*cda5da8dSAndroid Build Coastguard Worker target.write(line) 257*cda5da8dSAndroid Build Coastguard Worker lastline = line 258*cda5da8dSAndroid Build Coastguard Worker if self._append_newline and lastline and not lastline.endswith(linesep): 259*cda5da8dSAndroid Build Coastguard Worker # Make sure the message ends with a newline 260*cda5da8dSAndroid Build Coastguard Worker target.write(linesep) 261*cda5da8dSAndroid Build Coastguard Worker else: 262*cda5da8dSAndroid Build Coastguard Worker raise TypeError('Invalid message type: %s' % type(message)) 263*cda5da8dSAndroid Build Coastguard Worker 264*cda5da8dSAndroid Build Coastguard Worker __class_getitem__ = classmethod(GenericAlias) 265*cda5da8dSAndroid Build Coastguard Worker 266*cda5da8dSAndroid Build Coastguard Worker 267*cda5da8dSAndroid Build Coastguard Workerclass Maildir(Mailbox): 268*cda5da8dSAndroid Build Coastguard Worker """A qmail-style Maildir mailbox.""" 269*cda5da8dSAndroid Build Coastguard Worker 270*cda5da8dSAndroid Build Coastguard Worker colon = ':' 271*cda5da8dSAndroid Build Coastguard Worker 272*cda5da8dSAndroid Build Coastguard Worker def __init__(self, dirname, factory=None, create=True): 273*cda5da8dSAndroid Build Coastguard Worker """Initialize a Maildir instance.""" 274*cda5da8dSAndroid Build Coastguard Worker Mailbox.__init__(self, dirname, factory, create) 275*cda5da8dSAndroid Build Coastguard Worker self._paths = { 276*cda5da8dSAndroid Build Coastguard Worker 'tmp': os.path.join(self._path, 'tmp'), 277*cda5da8dSAndroid Build Coastguard Worker 'new': os.path.join(self._path, 'new'), 278*cda5da8dSAndroid Build Coastguard Worker 'cur': os.path.join(self._path, 'cur'), 279*cda5da8dSAndroid Build Coastguard Worker } 280*cda5da8dSAndroid Build Coastguard Worker if not os.path.exists(self._path): 281*cda5da8dSAndroid Build Coastguard Worker if create: 282*cda5da8dSAndroid Build Coastguard Worker os.mkdir(self._path, 0o700) 283*cda5da8dSAndroid Build Coastguard Worker for path in self._paths.values(): 284*cda5da8dSAndroid Build Coastguard Worker os.mkdir(path, 0o700) 285*cda5da8dSAndroid Build Coastguard Worker else: 286*cda5da8dSAndroid Build Coastguard Worker raise NoSuchMailboxError(self._path) 287*cda5da8dSAndroid Build Coastguard Worker self._toc = {} 288*cda5da8dSAndroid Build Coastguard Worker self._toc_mtimes = {'cur': 0, 'new': 0} 289*cda5da8dSAndroid Build Coastguard Worker self._last_read = 0 # Records last time we read cur/new 290*cda5da8dSAndroid Build Coastguard Worker self._skewfactor = 0.1 # Adjust if os/fs clocks are skewing 291*cda5da8dSAndroid Build Coastguard Worker 292*cda5da8dSAndroid Build Coastguard Worker def add(self, message): 293*cda5da8dSAndroid Build Coastguard Worker """Add message and return assigned key.""" 294*cda5da8dSAndroid Build Coastguard Worker tmp_file = self._create_tmp() 295*cda5da8dSAndroid Build Coastguard Worker try: 296*cda5da8dSAndroid Build Coastguard Worker self._dump_message(message, tmp_file) 297*cda5da8dSAndroid Build Coastguard Worker except BaseException: 298*cda5da8dSAndroid Build Coastguard Worker tmp_file.close() 299*cda5da8dSAndroid Build Coastguard Worker os.remove(tmp_file.name) 300*cda5da8dSAndroid Build Coastguard Worker raise 301*cda5da8dSAndroid Build Coastguard Worker _sync_close(tmp_file) 302*cda5da8dSAndroid Build Coastguard Worker if isinstance(message, MaildirMessage): 303*cda5da8dSAndroid Build Coastguard Worker subdir = message.get_subdir() 304*cda5da8dSAndroid Build Coastguard Worker suffix = self.colon + message.get_info() 305*cda5da8dSAndroid Build Coastguard Worker if suffix == self.colon: 306*cda5da8dSAndroid Build Coastguard Worker suffix = '' 307*cda5da8dSAndroid Build Coastguard Worker else: 308*cda5da8dSAndroid Build Coastguard Worker subdir = 'new' 309*cda5da8dSAndroid Build Coastguard Worker suffix = '' 310*cda5da8dSAndroid Build Coastguard Worker uniq = os.path.basename(tmp_file.name).split(self.colon)[0] 311*cda5da8dSAndroid Build Coastguard Worker dest = os.path.join(self._path, subdir, uniq + suffix) 312*cda5da8dSAndroid Build Coastguard Worker if isinstance(message, MaildirMessage): 313*cda5da8dSAndroid Build Coastguard Worker os.utime(tmp_file.name, 314*cda5da8dSAndroid Build Coastguard Worker (os.path.getatime(tmp_file.name), message.get_date())) 315*cda5da8dSAndroid Build Coastguard Worker # No file modification should be done after the file is moved to its 316*cda5da8dSAndroid Build Coastguard Worker # final position in order to prevent race conditions with changes 317*cda5da8dSAndroid Build Coastguard Worker # from other programs 318*cda5da8dSAndroid Build Coastguard Worker try: 319*cda5da8dSAndroid Build Coastguard Worker try: 320*cda5da8dSAndroid Build Coastguard Worker os.link(tmp_file.name, dest) 321*cda5da8dSAndroid Build Coastguard Worker except (AttributeError, PermissionError): 322*cda5da8dSAndroid Build Coastguard Worker os.rename(tmp_file.name, dest) 323*cda5da8dSAndroid Build Coastguard Worker else: 324*cda5da8dSAndroid Build Coastguard Worker os.remove(tmp_file.name) 325*cda5da8dSAndroid Build Coastguard Worker except OSError as e: 326*cda5da8dSAndroid Build Coastguard Worker os.remove(tmp_file.name) 327*cda5da8dSAndroid Build Coastguard Worker if e.errno == errno.EEXIST: 328*cda5da8dSAndroid Build Coastguard Worker raise ExternalClashError('Name clash with existing message: %s' 329*cda5da8dSAndroid Build Coastguard Worker % dest) 330*cda5da8dSAndroid Build Coastguard Worker else: 331*cda5da8dSAndroid Build Coastguard Worker raise 332*cda5da8dSAndroid Build Coastguard Worker return uniq 333*cda5da8dSAndroid Build Coastguard Worker 334*cda5da8dSAndroid Build Coastguard Worker def remove(self, key): 335*cda5da8dSAndroid Build Coastguard Worker """Remove the keyed message; raise KeyError if it doesn't exist.""" 336*cda5da8dSAndroid Build Coastguard Worker os.remove(os.path.join(self._path, self._lookup(key))) 337*cda5da8dSAndroid Build Coastguard Worker 338*cda5da8dSAndroid Build Coastguard Worker def discard(self, key): 339*cda5da8dSAndroid Build Coastguard Worker """If the keyed message exists, remove it.""" 340*cda5da8dSAndroid Build Coastguard Worker # This overrides an inapplicable implementation in the superclass. 341*cda5da8dSAndroid Build Coastguard Worker try: 342*cda5da8dSAndroid Build Coastguard Worker self.remove(key) 343*cda5da8dSAndroid Build Coastguard Worker except (KeyError, FileNotFoundError): 344*cda5da8dSAndroid Build Coastguard Worker pass 345*cda5da8dSAndroid Build Coastguard Worker 346*cda5da8dSAndroid Build Coastguard Worker def __setitem__(self, key, message): 347*cda5da8dSAndroid Build Coastguard Worker """Replace the keyed message; raise KeyError if it doesn't exist.""" 348*cda5da8dSAndroid Build Coastguard Worker old_subpath = self._lookup(key) 349*cda5da8dSAndroid Build Coastguard Worker temp_key = self.add(message) 350*cda5da8dSAndroid Build Coastguard Worker temp_subpath = self._lookup(temp_key) 351*cda5da8dSAndroid Build Coastguard Worker if isinstance(message, MaildirMessage): 352*cda5da8dSAndroid Build Coastguard Worker # temp's subdir and suffix were specified by message. 353*cda5da8dSAndroid Build Coastguard Worker dominant_subpath = temp_subpath 354*cda5da8dSAndroid Build Coastguard Worker else: 355*cda5da8dSAndroid Build Coastguard Worker # temp's subdir and suffix were defaults from add(). 356*cda5da8dSAndroid Build Coastguard Worker dominant_subpath = old_subpath 357*cda5da8dSAndroid Build Coastguard Worker subdir = os.path.dirname(dominant_subpath) 358*cda5da8dSAndroid Build Coastguard Worker if self.colon in dominant_subpath: 359*cda5da8dSAndroid Build Coastguard Worker suffix = self.colon + dominant_subpath.split(self.colon)[-1] 360*cda5da8dSAndroid Build Coastguard Worker else: 361*cda5da8dSAndroid Build Coastguard Worker suffix = '' 362*cda5da8dSAndroid Build Coastguard Worker self.discard(key) 363*cda5da8dSAndroid Build Coastguard Worker tmp_path = os.path.join(self._path, temp_subpath) 364*cda5da8dSAndroid Build Coastguard Worker new_path = os.path.join(self._path, subdir, key + suffix) 365*cda5da8dSAndroid Build Coastguard Worker if isinstance(message, MaildirMessage): 366*cda5da8dSAndroid Build Coastguard Worker os.utime(tmp_path, 367*cda5da8dSAndroid Build Coastguard Worker (os.path.getatime(tmp_path), message.get_date())) 368*cda5da8dSAndroid Build Coastguard Worker # No file modification should be done after the file is moved to its 369*cda5da8dSAndroid Build Coastguard Worker # final position in order to prevent race conditions with changes 370*cda5da8dSAndroid Build Coastguard Worker # from other programs 371*cda5da8dSAndroid Build Coastguard Worker os.rename(tmp_path, new_path) 372*cda5da8dSAndroid Build Coastguard Worker 373*cda5da8dSAndroid Build Coastguard Worker def get_message(self, key): 374*cda5da8dSAndroid Build Coastguard Worker """Return a Message representation or raise a KeyError.""" 375*cda5da8dSAndroid Build Coastguard Worker subpath = self._lookup(key) 376*cda5da8dSAndroid Build Coastguard Worker with open(os.path.join(self._path, subpath), 'rb') as f: 377*cda5da8dSAndroid Build Coastguard Worker if self._factory: 378*cda5da8dSAndroid Build Coastguard Worker msg = self._factory(f) 379*cda5da8dSAndroid Build Coastguard Worker else: 380*cda5da8dSAndroid Build Coastguard Worker msg = MaildirMessage(f) 381*cda5da8dSAndroid Build Coastguard Worker subdir, name = os.path.split(subpath) 382*cda5da8dSAndroid Build Coastguard Worker msg.set_subdir(subdir) 383*cda5da8dSAndroid Build Coastguard Worker if self.colon in name: 384*cda5da8dSAndroid Build Coastguard Worker msg.set_info(name.split(self.colon)[-1]) 385*cda5da8dSAndroid Build Coastguard Worker msg.set_date(os.path.getmtime(os.path.join(self._path, subpath))) 386*cda5da8dSAndroid Build Coastguard Worker return msg 387*cda5da8dSAndroid Build Coastguard Worker 388*cda5da8dSAndroid Build Coastguard Worker def get_bytes(self, key): 389*cda5da8dSAndroid Build Coastguard Worker """Return a bytes representation or raise a KeyError.""" 390*cda5da8dSAndroid Build Coastguard Worker with open(os.path.join(self._path, self._lookup(key)), 'rb') as f: 391*cda5da8dSAndroid Build Coastguard Worker return f.read().replace(linesep, b'\n') 392*cda5da8dSAndroid Build Coastguard Worker 393*cda5da8dSAndroid Build Coastguard Worker def get_file(self, key): 394*cda5da8dSAndroid Build Coastguard Worker """Return a file-like representation or raise a KeyError.""" 395*cda5da8dSAndroid Build Coastguard Worker f = open(os.path.join(self._path, self._lookup(key)), 'rb') 396*cda5da8dSAndroid Build Coastguard Worker return _ProxyFile(f) 397*cda5da8dSAndroid Build Coastguard Worker 398*cda5da8dSAndroid Build Coastguard Worker def iterkeys(self): 399*cda5da8dSAndroid Build Coastguard Worker """Return an iterator over keys.""" 400*cda5da8dSAndroid Build Coastguard Worker self._refresh() 401*cda5da8dSAndroid Build Coastguard Worker for key in self._toc: 402*cda5da8dSAndroid Build Coastguard Worker try: 403*cda5da8dSAndroid Build Coastguard Worker self._lookup(key) 404*cda5da8dSAndroid Build Coastguard Worker except KeyError: 405*cda5da8dSAndroid Build Coastguard Worker continue 406*cda5da8dSAndroid Build Coastguard Worker yield key 407*cda5da8dSAndroid Build Coastguard Worker 408*cda5da8dSAndroid Build Coastguard Worker def __contains__(self, key): 409*cda5da8dSAndroid Build Coastguard Worker """Return True if the keyed message exists, False otherwise.""" 410*cda5da8dSAndroid Build Coastguard Worker self._refresh() 411*cda5da8dSAndroid Build Coastguard Worker return key in self._toc 412*cda5da8dSAndroid Build Coastguard Worker 413*cda5da8dSAndroid Build Coastguard Worker def __len__(self): 414*cda5da8dSAndroid Build Coastguard Worker """Return a count of messages in the mailbox.""" 415*cda5da8dSAndroid Build Coastguard Worker self._refresh() 416*cda5da8dSAndroid Build Coastguard Worker return len(self._toc) 417*cda5da8dSAndroid Build Coastguard Worker 418*cda5da8dSAndroid Build Coastguard Worker def flush(self): 419*cda5da8dSAndroid Build Coastguard Worker """Write any pending changes to disk.""" 420*cda5da8dSAndroid Build Coastguard Worker # Maildir changes are always written immediately, so there's nothing 421*cda5da8dSAndroid Build Coastguard Worker # to do. 422*cda5da8dSAndroid Build Coastguard Worker pass 423*cda5da8dSAndroid Build Coastguard Worker 424*cda5da8dSAndroid Build Coastguard Worker def lock(self): 425*cda5da8dSAndroid Build Coastguard Worker """Lock the mailbox.""" 426*cda5da8dSAndroid Build Coastguard Worker return 427*cda5da8dSAndroid Build Coastguard Worker 428*cda5da8dSAndroid Build Coastguard Worker def unlock(self): 429*cda5da8dSAndroid Build Coastguard Worker """Unlock the mailbox if it is locked.""" 430*cda5da8dSAndroid Build Coastguard Worker return 431*cda5da8dSAndroid Build Coastguard Worker 432*cda5da8dSAndroid Build Coastguard Worker def close(self): 433*cda5da8dSAndroid Build Coastguard Worker """Flush and close the mailbox.""" 434*cda5da8dSAndroid Build Coastguard Worker return 435*cda5da8dSAndroid Build Coastguard Worker 436*cda5da8dSAndroid Build Coastguard Worker def list_folders(self): 437*cda5da8dSAndroid Build Coastguard Worker """Return a list of folder names.""" 438*cda5da8dSAndroid Build Coastguard Worker result = [] 439*cda5da8dSAndroid Build Coastguard Worker for entry in os.listdir(self._path): 440*cda5da8dSAndroid Build Coastguard Worker if len(entry) > 1 and entry[0] == '.' and \ 441*cda5da8dSAndroid Build Coastguard Worker os.path.isdir(os.path.join(self._path, entry)): 442*cda5da8dSAndroid Build Coastguard Worker result.append(entry[1:]) 443*cda5da8dSAndroid Build Coastguard Worker return result 444*cda5da8dSAndroid Build Coastguard Worker 445*cda5da8dSAndroid Build Coastguard Worker def get_folder(self, folder): 446*cda5da8dSAndroid Build Coastguard Worker """Return a Maildir instance for the named folder.""" 447*cda5da8dSAndroid Build Coastguard Worker return Maildir(os.path.join(self._path, '.' + folder), 448*cda5da8dSAndroid Build Coastguard Worker factory=self._factory, 449*cda5da8dSAndroid Build Coastguard Worker create=False) 450*cda5da8dSAndroid Build Coastguard Worker 451*cda5da8dSAndroid Build Coastguard Worker def add_folder(self, folder): 452*cda5da8dSAndroid Build Coastguard Worker """Create a folder and return a Maildir instance representing it.""" 453*cda5da8dSAndroid Build Coastguard Worker path = os.path.join(self._path, '.' + folder) 454*cda5da8dSAndroid Build Coastguard Worker result = Maildir(path, factory=self._factory) 455*cda5da8dSAndroid Build Coastguard Worker maildirfolder_path = os.path.join(path, 'maildirfolder') 456*cda5da8dSAndroid Build Coastguard Worker if not os.path.exists(maildirfolder_path): 457*cda5da8dSAndroid Build Coastguard Worker os.close(os.open(maildirfolder_path, os.O_CREAT | os.O_WRONLY, 458*cda5da8dSAndroid Build Coastguard Worker 0o666)) 459*cda5da8dSAndroid Build Coastguard Worker return result 460*cda5da8dSAndroid Build Coastguard Worker 461*cda5da8dSAndroid Build Coastguard Worker def remove_folder(self, folder): 462*cda5da8dSAndroid Build Coastguard Worker """Delete the named folder, which must be empty.""" 463*cda5da8dSAndroid Build Coastguard Worker path = os.path.join(self._path, '.' + folder) 464*cda5da8dSAndroid Build Coastguard Worker for entry in os.listdir(os.path.join(path, 'new')) + \ 465*cda5da8dSAndroid Build Coastguard Worker os.listdir(os.path.join(path, 'cur')): 466*cda5da8dSAndroid Build Coastguard Worker if len(entry) < 1 or entry[0] != '.': 467*cda5da8dSAndroid Build Coastguard Worker raise NotEmptyError('Folder contains message(s): %s' % folder) 468*cda5da8dSAndroid Build Coastguard Worker for entry in os.listdir(path): 469*cda5da8dSAndroid Build Coastguard Worker if entry != 'new' and entry != 'cur' and entry != 'tmp' and \ 470*cda5da8dSAndroid Build Coastguard Worker os.path.isdir(os.path.join(path, entry)): 471*cda5da8dSAndroid Build Coastguard Worker raise NotEmptyError("Folder contains subdirectory '%s': %s" % 472*cda5da8dSAndroid Build Coastguard Worker (folder, entry)) 473*cda5da8dSAndroid Build Coastguard Worker for root, dirs, files in os.walk(path, topdown=False): 474*cda5da8dSAndroid Build Coastguard Worker for entry in files: 475*cda5da8dSAndroid Build Coastguard Worker os.remove(os.path.join(root, entry)) 476*cda5da8dSAndroid Build Coastguard Worker for entry in dirs: 477*cda5da8dSAndroid Build Coastguard Worker os.rmdir(os.path.join(root, entry)) 478*cda5da8dSAndroid Build Coastguard Worker os.rmdir(path) 479*cda5da8dSAndroid Build Coastguard Worker 480*cda5da8dSAndroid Build Coastguard Worker def clean(self): 481*cda5da8dSAndroid Build Coastguard Worker """Delete old files in "tmp".""" 482*cda5da8dSAndroid Build Coastguard Worker now = time.time() 483*cda5da8dSAndroid Build Coastguard Worker for entry in os.listdir(os.path.join(self._path, 'tmp')): 484*cda5da8dSAndroid Build Coastguard Worker path = os.path.join(self._path, 'tmp', entry) 485*cda5da8dSAndroid Build Coastguard Worker if now - os.path.getatime(path) > 129600: # 60 * 60 * 36 486*cda5da8dSAndroid Build Coastguard Worker os.remove(path) 487*cda5da8dSAndroid Build Coastguard Worker 488*cda5da8dSAndroid Build Coastguard Worker _count = 1 # This is used to generate unique file names. 489*cda5da8dSAndroid Build Coastguard Worker 490*cda5da8dSAndroid Build Coastguard Worker def _create_tmp(self): 491*cda5da8dSAndroid Build Coastguard Worker """Create a file in the tmp subdirectory and open and return it.""" 492*cda5da8dSAndroid Build Coastguard Worker now = time.time() 493*cda5da8dSAndroid Build Coastguard Worker hostname = socket.gethostname() 494*cda5da8dSAndroid Build Coastguard Worker if '/' in hostname: 495*cda5da8dSAndroid Build Coastguard Worker hostname = hostname.replace('/', r'\057') 496*cda5da8dSAndroid Build Coastguard Worker if ':' in hostname: 497*cda5da8dSAndroid Build Coastguard Worker hostname = hostname.replace(':', r'\072') 498*cda5da8dSAndroid Build Coastguard Worker uniq = "%s.M%sP%sQ%s.%s" % (int(now), int(now % 1 * 1e6), os.getpid(), 499*cda5da8dSAndroid Build Coastguard Worker Maildir._count, hostname) 500*cda5da8dSAndroid Build Coastguard Worker path = os.path.join(self._path, 'tmp', uniq) 501*cda5da8dSAndroid Build Coastguard Worker try: 502*cda5da8dSAndroid Build Coastguard Worker os.stat(path) 503*cda5da8dSAndroid Build Coastguard Worker except FileNotFoundError: 504*cda5da8dSAndroid Build Coastguard Worker Maildir._count += 1 505*cda5da8dSAndroid Build Coastguard Worker try: 506*cda5da8dSAndroid Build Coastguard Worker return _create_carefully(path) 507*cda5da8dSAndroid Build Coastguard Worker except FileExistsError: 508*cda5da8dSAndroid Build Coastguard Worker pass 509*cda5da8dSAndroid Build Coastguard Worker 510*cda5da8dSAndroid Build Coastguard Worker # Fall through to here if stat succeeded or open raised EEXIST. 511*cda5da8dSAndroid Build Coastguard Worker raise ExternalClashError('Name clash prevented file creation: %s' % 512*cda5da8dSAndroid Build Coastguard Worker path) 513*cda5da8dSAndroid Build Coastguard Worker 514*cda5da8dSAndroid Build Coastguard Worker def _refresh(self): 515*cda5da8dSAndroid Build Coastguard Worker """Update table of contents mapping.""" 516*cda5da8dSAndroid Build Coastguard Worker # If it has been less than two seconds since the last _refresh() call, 517*cda5da8dSAndroid Build Coastguard Worker # we have to unconditionally re-read the mailbox just in case it has 518*cda5da8dSAndroid Build Coastguard Worker # been modified, because os.path.mtime() has a 2 sec resolution in the 519*cda5da8dSAndroid Build Coastguard Worker # most common worst case (FAT) and a 1 sec resolution typically. This 520*cda5da8dSAndroid Build Coastguard Worker # results in a few unnecessary re-reads when _refresh() is called 521*cda5da8dSAndroid Build Coastguard Worker # multiple times in that interval, but once the clock ticks over, we 522*cda5da8dSAndroid Build Coastguard Worker # will only re-read as needed. Because the filesystem might be being 523*cda5da8dSAndroid Build Coastguard Worker # served by an independent system with its own clock, we record and 524*cda5da8dSAndroid Build Coastguard Worker # compare with the mtimes from the filesystem. Because the other 525*cda5da8dSAndroid Build Coastguard Worker # system's clock might be skewing relative to our clock, we add an 526*cda5da8dSAndroid Build Coastguard Worker # extra delta to our wait. The default is one tenth second, but is an 527*cda5da8dSAndroid Build Coastguard Worker # instance variable and so can be adjusted if dealing with a 528*cda5da8dSAndroid Build Coastguard Worker # particularly skewed or irregular system. 529*cda5da8dSAndroid Build Coastguard Worker if time.time() - self._last_read > 2 + self._skewfactor: 530*cda5da8dSAndroid Build Coastguard Worker refresh = False 531*cda5da8dSAndroid Build Coastguard Worker for subdir in self._toc_mtimes: 532*cda5da8dSAndroid Build Coastguard Worker mtime = os.path.getmtime(self._paths[subdir]) 533*cda5da8dSAndroid Build Coastguard Worker if mtime > self._toc_mtimes[subdir]: 534*cda5da8dSAndroid Build Coastguard Worker refresh = True 535*cda5da8dSAndroid Build Coastguard Worker self._toc_mtimes[subdir] = mtime 536*cda5da8dSAndroid Build Coastguard Worker if not refresh: 537*cda5da8dSAndroid Build Coastguard Worker return 538*cda5da8dSAndroid Build Coastguard Worker # Refresh toc 539*cda5da8dSAndroid Build Coastguard Worker self._toc = {} 540*cda5da8dSAndroid Build Coastguard Worker for subdir in self._toc_mtimes: 541*cda5da8dSAndroid Build Coastguard Worker path = self._paths[subdir] 542*cda5da8dSAndroid Build Coastguard Worker for entry in os.listdir(path): 543*cda5da8dSAndroid Build Coastguard Worker p = os.path.join(path, entry) 544*cda5da8dSAndroid Build Coastguard Worker if os.path.isdir(p): 545*cda5da8dSAndroid Build Coastguard Worker continue 546*cda5da8dSAndroid Build Coastguard Worker uniq = entry.split(self.colon)[0] 547*cda5da8dSAndroid Build Coastguard Worker self._toc[uniq] = os.path.join(subdir, entry) 548*cda5da8dSAndroid Build Coastguard Worker self._last_read = time.time() 549*cda5da8dSAndroid Build Coastguard Worker 550*cda5da8dSAndroid Build Coastguard Worker def _lookup(self, key): 551*cda5da8dSAndroid Build Coastguard Worker """Use TOC to return subpath for given key, or raise a KeyError.""" 552*cda5da8dSAndroid Build Coastguard Worker try: 553*cda5da8dSAndroid Build Coastguard Worker if os.path.exists(os.path.join(self._path, self._toc[key])): 554*cda5da8dSAndroid Build Coastguard Worker return self._toc[key] 555*cda5da8dSAndroid Build Coastguard Worker except KeyError: 556*cda5da8dSAndroid Build Coastguard Worker pass 557*cda5da8dSAndroid Build Coastguard Worker self._refresh() 558*cda5da8dSAndroid Build Coastguard Worker try: 559*cda5da8dSAndroid Build Coastguard Worker return self._toc[key] 560*cda5da8dSAndroid Build Coastguard Worker except KeyError: 561*cda5da8dSAndroid Build Coastguard Worker raise KeyError('No message with key: %s' % key) from None 562*cda5da8dSAndroid Build Coastguard Worker 563*cda5da8dSAndroid Build Coastguard Worker # This method is for backward compatibility only. 564*cda5da8dSAndroid Build Coastguard Worker def next(self): 565*cda5da8dSAndroid Build Coastguard Worker """Return the next message in a one-time iteration.""" 566*cda5da8dSAndroid Build Coastguard Worker if not hasattr(self, '_onetime_keys'): 567*cda5da8dSAndroid Build Coastguard Worker self._onetime_keys = self.iterkeys() 568*cda5da8dSAndroid Build Coastguard Worker while True: 569*cda5da8dSAndroid Build Coastguard Worker try: 570*cda5da8dSAndroid Build Coastguard Worker return self[next(self._onetime_keys)] 571*cda5da8dSAndroid Build Coastguard Worker except StopIteration: 572*cda5da8dSAndroid Build Coastguard Worker return None 573*cda5da8dSAndroid Build Coastguard Worker except KeyError: 574*cda5da8dSAndroid Build Coastguard Worker continue 575*cda5da8dSAndroid Build Coastguard Worker 576*cda5da8dSAndroid Build Coastguard Worker 577*cda5da8dSAndroid Build Coastguard Workerclass _singlefileMailbox(Mailbox): 578*cda5da8dSAndroid Build Coastguard Worker """A single-file mailbox.""" 579*cda5da8dSAndroid Build Coastguard Worker 580*cda5da8dSAndroid Build Coastguard Worker def __init__(self, path, factory=None, create=True): 581*cda5da8dSAndroid Build Coastguard Worker """Initialize a single-file mailbox.""" 582*cda5da8dSAndroid Build Coastguard Worker Mailbox.__init__(self, path, factory, create) 583*cda5da8dSAndroid Build Coastguard Worker try: 584*cda5da8dSAndroid Build Coastguard Worker f = open(self._path, 'rb+') 585*cda5da8dSAndroid Build Coastguard Worker except OSError as e: 586*cda5da8dSAndroid Build Coastguard Worker if e.errno == errno.ENOENT: 587*cda5da8dSAndroid Build Coastguard Worker if create: 588*cda5da8dSAndroid Build Coastguard Worker f = open(self._path, 'wb+') 589*cda5da8dSAndroid Build Coastguard Worker else: 590*cda5da8dSAndroid Build Coastguard Worker raise NoSuchMailboxError(self._path) 591*cda5da8dSAndroid Build Coastguard Worker elif e.errno in (errno.EACCES, errno.EROFS): 592*cda5da8dSAndroid Build Coastguard Worker f = open(self._path, 'rb') 593*cda5da8dSAndroid Build Coastguard Worker else: 594*cda5da8dSAndroid Build Coastguard Worker raise 595*cda5da8dSAndroid Build Coastguard Worker self._file = f 596*cda5da8dSAndroid Build Coastguard Worker self._toc = None 597*cda5da8dSAndroid Build Coastguard Worker self._next_key = 0 598*cda5da8dSAndroid Build Coastguard Worker self._pending = False # No changes require rewriting the file. 599*cda5da8dSAndroid Build Coastguard Worker self._pending_sync = False # No need to sync the file 600*cda5da8dSAndroid Build Coastguard Worker self._locked = False 601*cda5da8dSAndroid Build Coastguard Worker self._file_length = None # Used to record mailbox size 602*cda5da8dSAndroid Build Coastguard Worker 603*cda5da8dSAndroid Build Coastguard Worker def add(self, message): 604*cda5da8dSAndroid Build Coastguard Worker """Add message and return assigned key.""" 605*cda5da8dSAndroid Build Coastguard Worker self._lookup() 606*cda5da8dSAndroid Build Coastguard Worker self._toc[self._next_key] = self._append_message(message) 607*cda5da8dSAndroid Build Coastguard Worker self._next_key += 1 608*cda5da8dSAndroid Build Coastguard Worker # _append_message appends the message to the mailbox file. We 609*cda5da8dSAndroid Build Coastguard Worker # don't need a full rewrite + rename, sync is enough. 610*cda5da8dSAndroid Build Coastguard Worker self._pending_sync = True 611*cda5da8dSAndroid Build Coastguard Worker return self._next_key - 1 612*cda5da8dSAndroid Build Coastguard Worker 613*cda5da8dSAndroid Build Coastguard Worker def remove(self, key): 614*cda5da8dSAndroid Build Coastguard Worker """Remove the keyed message; raise KeyError if it doesn't exist.""" 615*cda5da8dSAndroid Build Coastguard Worker self._lookup(key) 616*cda5da8dSAndroid Build Coastguard Worker del self._toc[key] 617*cda5da8dSAndroid Build Coastguard Worker self._pending = True 618*cda5da8dSAndroid Build Coastguard Worker 619*cda5da8dSAndroid Build Coastguard Worker def __setitem__(self, key, message): 620*cda5da8dSAndroid Build Coastguard Worker """Replace the keyed message; raise KeyError if it doesn't exist.""" 621*cda5da8dSAndroid Build Coastguard Worker self._lookup(key) 622*cda5da8dSAndroid Build Coastguard Worker self._toc[key] = self._append_message(message) 623*cda5da8dSAndroid Build Coastguard Worker self._pending = True 624*cda5da8dSAndroid Build Coastguard Worker 625*cda5da8dSAndroid Build Coastguard Worker def iterkeys(self): 626*cda5da8dSAndroid Build Coastguard Worker """Return an iterator over keys.""" 627*cda5da8dSAndroid Build Coastguard Worker self._lookup() 628*cda5da8dSAndroid Build Coastguard Worker yield from self._toc.keys() 629*cda5da8dSAndroid Build Coastguard Worker 630*cda5da8dSAndroid Build Coastguard Worker def __contains__(self, key): 631*cda5da8dSAndroid Build Coastguard Worker """Return True if the keyed message exists, False otherwise.""" 632*cda5da8dSAndroid Build Coastguard Worker self._lookup() 633*cda5da8dSAndroid Build Coastguard Worker return key in self._toc 634*cda5da8dSAndroid Build Coastguard Worker 635*cda5da8dSAndroid Build Coastguard Worker def __len__(self): 636*cda5da8dSAndroid Build Coastguard Worker """Return a count of messages in the mailbox.""" 637*cda5da8dSAndroid Build Coastguard Worker self._lookup() 638*cda5da8dSAndroid Build Coastguard Worker return len(self._toc) 639*cda5da8dSAndroid Build Coastguard Worker 640*cda5da8dSAndroid Build Coastguard Worker def lock(self): 641*cda5da8dSAndroid Build Coastguard Worker """Lock the mailbox.""" 642*cda5da8dSAndroid Build Coastguard Worker if not self._locked: 643*cda5da8dSAndroid Build Coastguard Worker _lock_file(self._file) 644*cda5da8dSAndroid Build Coastguard Worker self._locked = True 645*cda5da8dSAndroid Build Coastguard Worker 646*cda5da8dSAndroid Build Coastguard Worker def unlock(self): 647*cda5da8dSAndroid Build Coastguard Worker """Unlock the mailbox if it is locked.""" 648*cda5da8dSAndroid Build Coastguard Worker if self._locked: 649*cda5da8dSAndroid Build Coastguard Worker _unlock_file(self._file) 650*cda5da8dSAndroid Build Coastguard Worker self._locked = False 651*cda5da8dSAndroid Build Coastguard Worker 652*cda5da8dSAndroid Build Coastguard Worker def flush(self): 653*cda5da8dSAndroid Build Coastguard Worker """Write any pending changes to disk.""" 654*cda5da8dSAndroid Build Coastguard Worker if not self._pending: 655*cda5da8dSAndroid Build Coastguard Worker if self._pending_sync: 656*cda5da8dSAndroid Build Coastguard Worker # Messages have only been added, so syncing the file 657*cda5da8dSAndroid Build Coastguard Worker # is enough. 658*cda5da8dSAndroid Build Coastguard Worker _sync_flush(self._file) 659*cda5da8dSAndroid Build Coastguard Worker self._pending_sync = False 660*cda5da8dSAndroid Build Coastguard Worker return 661*cda5da8dSAndroid Build Coastguard Worker 662*cda5da8dSAndroid Build Coastguard Worker # In order to be writing anything out at all, self._toc must 663*cda5da8dSAndroid Build Coastguard Worker # already have been generated (and presumably has been modified 664*cda5da8dSAndroid Build Coastguard Worker # by adding or deleting an item). 665*cda5da8dSAndroid Build Coastguard Worker assert self._toc is not None 666*cda5da8dSAndroid Build Coastguard Worker 667*cda5da8dSAndroid Build Coastguard Worker # Check length of self._file; if it's changed, some other process 668*cda5da8dSAndroid Build Coastguard Worker # has modified the mailbox since we scanned it. 669*cda5da8dSAndroid Build Coastguard Worker self._file.seek(0, 2) 670*cda5da8dSAndroid Build Coastguard Worker cur_len = self._file.tell() 671*cda5da8dSAndroid Build Coastguard Worker if cur_len != self._file_length: 672*cda5da8dSAndroid Build Coastguard Worker raise ExternalClashError('Size of mailbox file changed ' 673*cda5da8dSAndroid Build Coastguard Worker '(expected %i, found %i)' % 674*cda5da8dSAndroid Build Coastguard Worker (self._file_length, cur_len)) 675*cda5da8dSAndroid Build Coastguard Worker 676*cda5da8dSAndroid Build Coastguard Worker new_file = _create_temporary(self._path) 677*cda5da8dSAndroid Build Coastguard Worker try: 678*cda5da8dSAndroid Build Coastguard Worker new_toc = {} 679*cda5da8dSAndroid Build Coastguard Worker self._pre_mailbox_hook(new_file) 680*cda5da8dSAndroid Build Coastguard Worker for key in sorted(self._toc.keys()): 681*cda5da8dSAndroid Build Coastguard Worker start, stop = self._toc[key] 682*cda5da8dSAndroid Build Coastguard Worker self._file.seek(start) 683*cda5da8dSAndroid Build Coastguard Worker self._pre_message_hook(new_file) 684*cda5da8dSAndroid Build Coastguard Worker new_start = new_file.tell() 685*cda5da8dSAndroid Build Coastguard Worker while True: 686*cda5da8dSAndroid Build Coastguard Worker buffer = self._file.read(min(4096, 687*cda5da8dSAndroid Build Coastguard Worker stop - self._file.tell())) 688*cda5da8dSAndroid Build Coastguard Worker if not buffer: 689*cda5da8dSAndroid Build Coastguard Worker break 690*cda5da8dSAndroid Build Coastguard Worker new_file.write(buffer) 691*cda5da8dSAndroid Build Coastguard Worker new_toc[key] = (new_start, new_file.tell()) 692*cda5da8dSAndroid Build Coastguard Worker self._post_message_hook(new_file) 693*cda5da8dSAndroid Build Coastguard Worker self._file_length = new_file.tell() 694*cda5da8dSAndroid Build Coastguard Worker except: 695*cda5da8dSAndroid Build Coastguard Worker new_file.close() 696*cda5da8dSAndroid Build Coastguard Worker os.remove(new_file.name) 697*cda5da8dSAndroid Build Coastguard Worker raise 698*cda5da8dSAndroid Build Coastguard Worker _sync_close(new_file) 699*cda5da8dSAndroid Build Coastguard Worker # self._file is about to get replaced, so no need to sync. 700*cda5da8dSAndroid Build Coastguard Worker self._file.close() 701*cda5da8dSAndroid Build Coastguard Worker # Make sure the new file's mode is the same as the old file's 702*cda5da8dSAndroid Build Coastguard Worker mode = os.stat(self._path).st_mode 703*cda5da8dSAndroid Build Coastguard Worker os.chmod(new_file.name, mode) 704*cda5da8dSAndroid Build Coastguard Worker try: 705*cda5da8dSAndroid Build Coastguard Worker os.rename(new_file.name, self._path) 706*cda5da8dSAndroid Build Coastguard Worker except FileExistsError: 707*cda5da8dSAndroid Build Coastguard Worker os.remove(self._path) 708*cda5da8dSAndroid Build Coastguard Worker os.rename(new_file.name, self._path) 709*cda5da8dSAndroid Build Coastguard Worker self._file = open(self._path, 'rb+') 710*cda5da8dSAndroid Build Coastguard Worker self._toc = new_toc 711*cda5da8dSAndroid Build Coastguard Worker self._pending = False 712*cda5da8dSAndroid Build Coastguard Worker self._pending_sync = False 713*cda5da8dSAndroid Build Coastguard Worker if self._locked: 714*cda5da8dSAndroid Build Coastguard Worker _lock_file(self._file, dotlock=False) 715*cda5da8dSAndroid Build Coastguard Worker 716*cda5da8dSAndroid Build Coastguard Worker def _pre_mailbox_hook(self, f): 717*cda5da8dSAndroid Build Coastguard Worker """Called before writing the mailbox to file f.""" 718*cda5da8dSAndroid Build Coastguard Worker return 719*cda5da8dSAndroid Build Coastguard Worker 720*cda5da8dSAndroid Build Coastguard Worker def _pre_message_hook(self, f): 721*cda5da8dSAndroid Build Coastguard Worker """Called before writing each message to file f.""" 722*cda5da8dSAndroid Build Coastguard Worker return 723*cda5da8dSAndroid Build Coastguard Worker 724*cda5da8dSAndroid Build Coastguard Worker def _post_message_hook(self, f): 725*cda5da8dSAndroid Build Coastguard Worker """Called after writing each message to file f.""" 726*cda5da8dSAndroid Build Coastguard Worker return 727*cda5da8dSAndroid Build Coastguard Worker 728*cda5da8dSAndroid Build Coastguard Worker def close(self): 729*cda5da8dSAndroid Build Coastguard Worker """Flush and close the mailbox.""" 730*cda5da8dSAndroid Build Coastguard Worker try: 731*cda5da8dSAndroid Build Coastguard Worker self.flush() 732*cda5da8dSAndroid Build Coastguard Worker finally: 733*cda5da8dSAndroid Build Coastguard Worker try: 734*cda5da8dSAndroid Build Coastguard Worker if self._locked: 735*cda5da8dSAndroid Build Coastguard Worker self.unlock() 736*cda5da8dSAndroid Build Coastguard Worker finally: 737*cda5da8dSAndroid Build Coastguard Worker self._file.close() # Sync has been done by self.flush() above. 738*cda5da8dSAndroid Build Coastguard Worker 739*cda5da8dSAndroid Build Coastguard Worker def _lookup(self, key=None): 740*cda5da8dSAndroid Build Coastguard Worker """Return (start, stop) or raise KeyError.""" 741*cda5da8dSAndroid Build Coastguard Worker if self._toc is None: 742*cda5da8dSAndroid Build Coastguard Worker self._generate_toc() 743*cda5da8dSAndroid Build Coastguard Worker if key is not None: 744*cda5da8dSAndroid Build Coastguard Worker try: 745*cda5da8dSAndroid Build Coastguard Worker return self._toc[key] 746*cda5da8dSAndroid Build Coastguard Worker except KeyError: 747*cda5da8dSAndroid Build Coastguard Worker raise KeyError('No message with key: %s' % key) from None 748*cda5da8dSAndroid Build Coastguard Worker 749*cda5da8dSAndroid Build Coastguard Worker def _append_message(self, message): 750*cda5da8dSAndroid Build Coastguard Worker """Append message to mailbox and return (start, stop) offsets.""" 751*cda5da8dSAndroid Build Coastguard Worker self._file.seek(0, 2) 752*cda5da8dSAndroid Build Coastguard Worker before = self._file.tell() 753*cda5da8dSAndroid Build Coastguard Worker if len(self._toc) == 0 and not self._pending: 754*cda5da8dSAndroid Build Coastguard Worker # This is the first message, and the _pre_mailbox_hook 755*cda5da8dSAndroid Build Coastguard Worker # hasn't yet been called. If self._pending is True, 756*cda5da8dSAndroid Build Coastguard Worker # messages have been removed, so _pre_mailbox_hook must 757*cda5da8dSAndroid Build Coastguard Worker # have been called already. 758*cda5da8dSAndroid Build Coastguard Worker self._pre_mailbox_hook(self._file) 759*cda5da8dSAndroid Build Coastguard Worker try: 760*cda5da8dSAndroid Build Coastguard Worker self._pre_message_hook(self._file) 761*cda5da8dSAndroid Build Coastguard Worker offsets = self._install_message(message) 762*cda5da8dSAndroid Build Coastguard Worker self._post_message_hook(self._file) 763*cda5da8dSAndroid Build Coastguard Worker except BaseException: 764*cda5da8dSAndroid Build Coastguard Worker self._file.truncate(before) 765*cda5da8dSAndroid Build Coastguard Worker raise 766*cda5da8dSAndroid Build Coastguard Worker self._file.flush() 767*cda5da8dSAndroid Build Coastguard Worker self._file_length = self._file.tell() # Record current length of mailbox 768*cda5da8dSAndroid Build Coastguard Worker return offsets 769*cda5da8dSAndroid Build Coastguard Worker 770*cda5da8dSAndroid Build Coastguard Worker 771*cda5da8dSAndroid Build Coastguard Worker 772*cda5da8dSAndroid Build Coastguard Workerclass _mboxMMDF(_singlefileMailbox): 773*cda5da8dSAndroid Build Coastguard Worker """An mbox or MMDF mailbox.""" 774*cda5da8dSAndroid Build Coastguard Worker 775*cda5da8dSAndroid Build Coastguard Worker _mangle_from_ = True 776*cda5da8dSAndroid Build Coastguard Worker 777*cda5da8dSAndroid Build Coastguard Worker def get_message(self, key): 778*cda5da8dSAndroid Build Coastguard Worker """Return a Message representation or raise a KeyError.""" 779*cda5da8dSAndroid Build Coastguard Worker start, stop = self._lookup(key) 780*cda5da8dSAndroid Build Coastguard Worker self._file.seek(start) 781*cda5da8dSAndroid Build Coastguard Worker from_line = self._file.readline().replace(linesep, b'') 782*cda5da8dSAndroid Build Coastguard Worker string = self._file.read(stop - self._file.tell()) 783*cda5da8dSAndroid Build Coastguard Worker msg = self._message_factory(string.replace(linesep, b'\n')) 784*cda5da8dSAndroid Build Coastguard Worker msg.set_from(from_line[5:].decode('ascii')) 785*cda5da8dSAndroid Build Coastguard Worker return msg 786*cda5da8dSAndroid Build Coastguard Worker 787*cda5da8dSAndroid Build Coastguard Worker def get_string(self, key, from_=False): 788*cda5da8dSAndroid Build Coastguard Worker """Return a string representation or raise a KeyError.""" 789*cda5da8dSAndroid Build Coastguard Worker return email.message_from_bytes( 790*cda5da8dSAndroid Build Coastguard Worker self.get_bytes(key, from_)).as_string(unixfrom=from_) 791*cda5da8dSAndroid Build Coastguard Worker 792*cda5da8dSAndroid Build Coastguard Worker def get_bytes(self, key, from_=False): 793*cda5da8dSAndroid Build Coastguard Worker """Return a string representation or raise a KeyError.""" 794*cda5da8dSAndroid Build Coastguard Worker start, stop = self._lookup(key) 795*cda5da8dSAndroid Build Coastguard Worker self._file.seek(start) 796*cda5da8dSAndroid Build Coastguard Worker if not from_: 797*cda5da8dSAndroid Build Coastguard Worker self._file.readline() 798*cda5da8dSAndroid Build Coastguard Worker string = self._file.read(stop - self._file.tell()) 799*cda5da8dSAndroid Build Coastguard Worker return string.replace(linesep, b'\n') 800*cda5da8dSAndroid Build Coastguard Worker 801*cda5da8dSAndroid Build Coastguard Worker def get_file(self, key, from_=False): 802*cda5da8dSAndroid Build Coastguard Worker """Return a file-like representation or raise a KeyError.""" 803*cda5da8dSAndroid Build Coastguard Worker start, stop = self._lookup(key) 804*cda5da8dSAndroid Build Coastguard Worker self._file.seek(start) 805*cda5da8dSAndroid Build Coastguard Worker if not from_: 806*cda5da8dSAndroid Build Coastguard Worker self._file.readline() 807*cda5da8dSAndroid Build Coastguard Worker return _PartialFile(self._file, self._file.tell(), stop) 808*cda5da8dSAndroid Build Coastguard Worker 809*cda5da8dSAndroid Build Coastguard Worker def _install_message(self, message): 810*cda5da8dSAndroid Build Coastguard Worker """Format a message and blindly write to self._file.""" 811*cda5da8dSAndroid Build Coastguard Worker from_line = None 812*cda5da8dSAndroid Build Coastguard Worker if isinstance(message, str): 813*cda5da8dSAndroid Build Coastguard Worker message = self._string_to_bytes(message) 814*cda5da8dSAndroid Build Coastguard Worker if isinstance(message, bytes) and message.startswith(b'From '): 815*cda5da8dSAndroid Build Coastguard Worker newline = message.find(b'\n') 816*cda5da8dSAndroid Build Coastguard Worker if newline != -1: 817*cda5da8dSAndroid Build Coastguard Worker from_line = message[:newline] 818*cda5da8dSAndroid Build Coastguard Worker message = message[newline + 1:] 819*cda5da8dSAndroid Build Coastguard Worker else: 820*cda5da8dSAndroid Build Coastguard Worker from_line = message 821*cda5da8dSAndroid Build Coastguard Worker message = b'' 822*cda5da8dSAndroid Build Coastguard Worker elif isinstance(message, _mboxMMDFMessage): 823*cda5da8dSAndroid Build Coastguard Worker author = message.get_from().encode('ascii') 824*cda5da8dSAndroid Build Coastguard Worker from_line = b'From ' + author 825*cda5da8dSAndroid Build Coastguard Worker elif isinstance(message, email.message.Message): 826*cda5da8dSAndroid Build Coastguard Worker from_line = message.get_unixfrom() # May be None. 827*cda5da8dSAndroid Build Coastguard Worker if from_line is not None: 828*cda5da8dSAndroid Build Coastguard Worker from_line = from_line.encode('ascii') 829*cda5da8dSAndroid Build Coastguard Worker if from_line is None: 830*cda5da8dSAndroid Build Coastguard Worker from_line = b'From MAILER-DAEMON ' + time.asctime(time.gmtime()).encode() 831*cda5da8dSAndroid Build Coastguard Worker start = self._file.tell() 832*cda5da8dSAndroid Build Coastguard Worker self._file.write(from_line + linesep) 833*cda5da8dSAndroid Build Coastguard Worker self._dump_message(message, self._file, self._mangle_from_) 834*cda5da8dSAndroid Build Coastguard Worker stop = self._file.tell() 835*cda5da8dSAndroid Build Coastguard Worker return (start, stop) 836*cda5da8dSAndroid Build Coastguard Worker 837*cda5da8dSAndroid Build Coastguard Worker 838*cda5da8dSAndroid Build Coastguard Workerclass mbox(_mboxMMDF): 839*cda5da8dSAndroid Build Coastguard Worker """A classic mbox mailbox.""" 840*cda5da8dSAndroid Build Coastguard Worker 841*cda5da8dSAndroid Build Coastguard Worker _mangle_from_ = True 842*cda5da8dSAndroid Build Coastguard Worker 843*cda5da8dSAndroid Build Coastguard Worker # All messages must end in a newline character, and 844*cda5da8dSAndroid Build Coastguard Worker # _post_message_hooks outputs an empty line between messages. 845*cda5da8dSAndroid Build Coastguard Worker _append_newline = True 846*cda5da8dSAndroid Build Coastguard Worker 847*cda5da8dSAndroid Build Coastguard Worker def __init__(self, path, factory=None, create=True): 848*cda5da8dSAndroid Build Coastguard Worker """Initialize an mbox mailbox.""" 849*cda5da8dSAndroid Build Coastguard Worker self._message_factory = mboxMessage 850*cda5da8dSAndroid Build Coastguard Worker _mboxMMDF.__init__(self, path, factory, create) 851*cda5da8dSAndroid Build Coastguard Worker 852*cda5da8dSAndroid Build Coastguard Worker def _post_message_hook(self, f): 853*cda5da8dSAndroid Build Coastguard Worker """Called after writing each message to file f.""" 854*cda5da8dSAndroid Build Coastguard Worker f.write(linesep) 855*cda5da8dSAndroid Build Coastguard Worker 856*cda5da8dSAndroid Build Coastguard Worker def _generate_toc(self): 857*cda5da8dSAndroid Build Coastguard Worker """Generate key-to-(start, stop) table of contents.""" 858*cda5da8dSAndroid Build Coastguard Worker starts, stops = [], [] 859*cda5da8dSAndroid Build Coastguard Worker last_was_empty = False 860*cda5da8dSAndroid Build Coastguard Worker self._file.seek(0) 861*cda5da8dSAndroid Build Coastguard Worker while True: 862*cda5da8dSAndroid Build Coastguard Worker line_pos = self._file.tell() 863*cda5da8dSAndroid Build Coastguard Worker line = self._file.readline() 864*cda5da8dSAndroid Build Coastguard Worker if line.startswith(b'From '): 865*cda5da8dSAndroid Build Coastguard Worker if len(stops) < len(starts): 866*cda5da8dSAndroid Build Coastguard Worker if last_was_empty: 867*cda5da8dSAndroid Build Coastguard Worker stops.append(line_pos - len(linesep)) 868*cda5da8dSAndroid Build Coastguard Worker else: 869*cda5da8dSAndroid Build Coastguard Worker # The last line before the "From " line wasn't 870*cda5da8dSAndroid Build Coastguard Worker # blank, but we consider it a start of a 871*cda5da8dSAndroid Build Coastguard Worker # message anyway. 872*cda5da8dSAndroid Build Coastguard Worker stops.append(line_pos) 873*cda5da8dSAndroid Build Coastguard Worker starts.append(line_pos) 874*cda5da8dSAndroid Build Coastguard Worker last_was_empty = False 875*cda5da8dSAndroid Build Coastguard Worker elif not line: 876*cda5da8dSAndroid Build Coastguard Worker if last_was_empty: 877*cda5da8dSAndroid Build Coastguard Worker stops.append(line_pos - len(linesep)) 878*cda5da8dSAndroid Build Coastguard Worker else: 879*cda5da8dSAndroid Build Coastguard Worker stops.append(line_pos) 880*cda5da8dSAndroid Build Coastguard Worker break 881*cda5da8dSAndroid Build Coastguard Worker elif line == linesep: 882*cda5da8dSAndroid Build Coastguard Worker last_was_empty = True 883*cda5da8dSAndroid Build Coastguard Worker else: 884*cda5da8dSAndroid Build Coastguard Worker last_was_empty = False 885*cda5da8dSAndroid Build Coastguard Worker self._toc = dict(enumerate(zip(starts, stops))) 886*cda5da8dSAndroid Build Coastguard Worker self._next_key = len(self._toc) 887*cda5da8dSAndroid Build Coastguard Worker self._file_length = self._file.tell() 888*cda5da8dSAndroid Build Coastguard Worker 889*cda5da8dSAndroid Build Coastguard Worker 890*cda5da8dSAndroid Build Coastguard Workerclass MMDF(_mboxMMDF): 891*cda5da8dSAndroid Build Coastguard Worker """An MMDF mailbox.""" 892*cda5da8dSAndroid Build Coastguard Worker 893*cda5da8dSAndroid Build Coastguard Worker def __init__(self, path, factory=None, create=True): 894*cda5da8dSAndroid Build Coastguard Worker """Initialize an MMDF mailbox.""" 895*cda5da8dSAndroid Build Coastguard Worker self._message_factory = MMDFMessage 896*cda5da8dSAndroid Build Coastguard Worker _mboxMMDF.__init__(self, path, factory, create) 897*cda5da8dSAndroid Build Coastguard Worker 898*cda5da8dSAndroid Build Coastguard Worker def _pre_message_hook(self, f): 899*cda5da8dSAndroid Build Coastguard Worker """Called before writing each message to file f.""" 900*cda5da8dSAndroid Build Coastguard Worker f.write(b'\001\001\001\001' + linesep) 901*cda5da8dSAndroid Build Coastguard Worker 902*cda5da8dSAndroid Build Coastguard Worker def _post_message_hook(self, f): 903*cda5da8dSAndroid Build Coastguard Worker """Called after writing each message to file f.""" 904*cda5da8dSAndroid Build Coastguard Worker f.write(linesep + b'\001\001\001\001' + linesep) 905*cda5da8dSAndroid Build Coastguard Worker 906*cda5da8dSAndroid Build Coastguard Worker def _generate_toc(self): 907*cda5da8dSAndroid Build Coastguard Worker """Generate key-to-(start, stop) table of contents.""" 908*cda5da8dSAndroid Build Coastguard Worker starts, stops = [], [] 909*cda5da8dSAndroid Build Coastguard Worker self._file.seek(0) 910*cda5da8dSAndroid Build Coastguard Worker next_pos = 0 911*cda5da8dSAndroid Build Coastguard Worker while True: 912*cda5da8dSAndroid Build Coastguard Worker line_pos = next_pos 913*cda5da8dSAndroid Build Coastguard Worker line = self._file.readline() 914*cda5da8dSAndroid Build Coastguard Worker next_pos = self._file.tell() 915*cda5da8dSAndroid Build Coastguard Worker if line.startswith(b'\001\001\001\001' + linesep): 916*cda5da8dSAndroid Build Coastguard Worker starts.append(next_pos) 917*cda5da8dSAndroid Build Coastguard Worker while True: 918*cda5da8dSAndroid Build Coastguard Worker line_pos = next_pos 919*cda5da8dSAndroid Build Coastguard Worker line = self._file.readline() 920*cda5da8dSAndroid Build Coastguard Worker next_pos = self._file.tell() 921*cda5da8dSAndroid Build Coastguard Worker if line == b'\001\001\001\001' + linesep: 922*cda5da8dSAndroid Build Coastguard Worker stops.append(line_pos - len(linesep)) 923*cda5da8dSAndroid Build Coastguard Worker break 924*cda5da8dSAndroid Build Coastguard Worker elif not line: 925*cda5da8dSAndroid Build Coastguard Worker stops.append(line_pos) 926*cda5da8dSAndroid Build Coastguard Worker break 927*cda5da8dSAndroid Build Coastguard Worker elif not line: 928*cda5da8dSAndroid Build Coastguard Worker break 929*cda5da8dSAndroid Build Coastguard Worker self._toc = dict(enumerate(zip(starts, stops))) 930*cda5da8dSAndroid Build Coastguard Worker self._next_key = len(self._toc) 931*cda5da8dSAndroid Build Coastguard Worker self._file.seek(0, 2) 932*cda5da8dSAndroid Build Coastguard Worker self._file_length = self._file.tell() 933*cda5da8dSAndroid Build Coastguard Worker 934*cda5da8dSAndroid Build Coastguard Worker 935*cda5da8dSAndroid Build Coastguard Workerclass MH(Mailbox): 936*cda5da8dSAndroid Build Coastguard Worker """An MH mailbox.""" 937*cda5da8dSAndroid Build Coastguard Worker 938*cda5da8dSAndroid Build Coastguard Worker def __init__(self, path, factory=None, create=True): 939*cda5da8dSAndroid Build Coastguard Worker """Initialize an MH instance.""" 940*cda5da8dSAndroid Build Coastguard Worker Mailbox.__init__(self, path, factory, create) 941*cda5da8dSAndroid Build Coastguard Worker if not os.path.exists(self._path): 942*cda5da8dSAndroid Build Coastguard Worker if create: 943*cda5da8dSAndroid Build Coastguard Worker os.mkdir(self._path, 0o700) 944*cda5da8dSAndroid Build Coastguard Worker os.close(os.open(os.path.join(self._path, '.mh_sequences'), 945*cda5da8dSAndroid Build Coastguard Worker os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)) 946*cda5da8dSAndroid Build Coastguard Worker else: 947*cda5da8dSAndroid Build Coastguard Worker raise NoSuchMailboxError(self._path) 948*cda5da8dSAndroid Build Coastguard Worker self._locked = False 949*cda5da8dSAndroid Build Coastguard Worker 950*cda5da8dSAndroid Build Coastguard Worker def add(self, message): 951*cda5da8dSAndroid Build Coastguard Worker """Add message and return assigned key.""" 952*cda5da8dSAndroid Build Coastguard Worker keys = self.keys() 953*cda5da8dSAndroid Build Coastguard Worker if len(keys) == 0: 954*cda5da8dSAndroid Build Coastguard Worker new_key = 1 955*cda5da8dSAndroid Build Coastguard Worker else: 956*cda5da8dSAndroid Build Coastguard Worker new_key = max(keys) + 1 957*cda5da8dSAndroid Build Coastguard Worker new_path = os.path.join(self._path, str(new_key)) 958*cda5da8dSAndroid Build Coastguard Worker f = _create_carefully(new_path) 959*cda5da8dSAndroid Build Coastguard Worker closed = False 960*cda5da8dSAndroid Build Coastguard Worker try: 961*cda5da8dSAndroid Build Coastguard Worker if self._locked: 962*cda5da8dSAndroid Build Coastguard Worker _lock_file(f) 963*cda5da8dSAndroid Build Coastguard Worker try: 964*cda5da8dSAndroid Build Coastguard Worker try: 965*cda5da8dSAndroid Build Coastguard Worker self._dump_message(message, f) 966*cda5da8dSAndroid Build Coastguard Worker except BaseException: 967*cda5da8dSAndroid Build Coastguard Worker # Unlock and close so it can be deleted on Windows 968*cda5da8dSAndroid Build Coastguard Worker if self._locked: 969*cda5da8dSAndroid Build Coastguard Worker _unlock_file(f) 970*cda5da8dSAndroid Build Coastguard Worker _sync_close(f) 971*cda5da8dSAndroid Build Coastguard Worker closed = True 972*cda5da8dSAndroid Build Coastguard Worker os.remove(new_path) 973*cda5da8dSAndroid Build Coastguard Worker raise 974*cda5da8dSAndroid Build Coastguard Worker if isinstance(message, MHMessage): 975*cda5da8dSAndroid Build Coastguard Worker self._dump_sequences(message, new_key) 976*cda5da8dSAndroid Build Coastguard Worker finally: 977*cda5da8dSAndroid Build Coastguard Worker if self._locked: 978*cda5da8dSAndroid Build Coastguard Worker _unlock_file(f) 979*cda5da8dSAndroid Build Coastguard Worker finally: 980*cda5da8dSAndroid Build Coastguard Worker if not closed: 981*cda5da8dSAndroid Build Coastguard Worker _sync_close(f) 982*cda5da8dSAndroid Build Coastguard Worker return new_key 983*cda5da8dSAndroid Build Coastguard Worker 984*cda5da8dSAndroid Build Coastguard Worker def remove(self, key): 985*cda5da8dSAndroid Build Coastguard Worker """Remove the keyed message; raise KeyError if it doesn't exist.""" 986*cda5da8dSAndroid Build Coastguard Worker path = os.path.join(self._path, str(key)) 987*cda5da8dSAndroid Build Coastguard Worker try: 988*cda5da8dSAndroid Build Coastguard Worker f = open(path, 'rb+') 989*cda5da8dSAndroid Build Coastguard Worker except OSError as e: 990*cda5da8dSAndroid Build Coastguard Worker if e.errno == errno.ENOENT: 991*cda5da8dSAndroid Build Coastguard Worker raise KeyError('No message with key: %s' % key) 992*cda5da8dSAndroid Build Coastguard Worker else: 993*cda5da8dSAndroid Build Coastguard Worker raise 994*cda5da8dSAndroid Build Coastguard Worker else: 995*cda5da8dSAndroid Build Coastguard Worker f.close() 996*cda5da8dSAndroid Build Coastguard Worker os.remove(path) 997*cda5da8dSAndroid Build Coastguard Worker 998*cda5da8dSAndroid Build Coastguard Worker def __setitem__(self, key, message): 999*cda5da8dSAndroid Build Coastguard Worker """Replace the keyed message; raise KeyError if it doesn't exist.""" 1000*cda5da8dSAndroid Build Coastguard Worker path = os.path.join(self._path, str(key)) 1001*cda5da8dSAndroid Build Coastguard Worker try: 1002*cda5da8dSAndroid Build Coastguard Worker f = open(path, 'rb+') 1003*cda5da8dSAndroid Build Coastguard Worker except OSError as e: 1004*cda5da8dSAndroid Build Coastguard Worker if e.errno == errno.ENOENT: 1005*cda5da8dSAndroid Build Coastguard Worker raise KeyError('No message with key: %s' % key) 1006*cda5da8dSAndroid Build Coastguard Worker else: 1007*cda5da8dSAndroid Build Coastguard Worker raise 1008*cda5da8dSAndroid Build Coastguard Worker try: 1009*cda5da8dSAndroid Build Coastguard Worker if self._locked: 1010*cda5da8dSAndroid Build Coastguard Worker _lock_file(f) 1011*cda5da8dSAndroid Build Coastguard Worker try: 1012*cda5da8dSAndroid Build Coastguard Worker os.close(os.open(path, os.O_WRONLY | os.O_TRUNC)) 1013*cda5da8dSAndroid Build Coastguard Worker self._dump_message(message, f) 1014*cda5da8dSAndroid Build Coastguard Worker if isinstance(message, MHMessage): 1015*cda5da8dSAndroid Build Coastguard Worker self._dump_sequences(message, key) 1016*cda5da8dSAndroid Build Coastguard Worker finally: 1017*cda5da8dSAndroid Build Coastguard Worker if self._locked: 1018*cda5da8dSAndroid Build Coastguard Worker _unlock_file(f) 1019*cda5da8dSAndroid Build Coastguard Worker finally: 1020*cda5da8dSAndroid Build Coastguard Worker _sync_close(f) 1021*cda5da8dSAndroid Build Coastguard Worker 1022*cda5da8dSAndroid Build Coastguard Worker def get_message(self, key): 1023*cda5da8dSAndroid Build Coastguard Worker """Return a Message representation or raise a KeyError.""" 1024*cda5da8dSAndroid Build Coastguard Worker try: 1025*cda5da8dSAndroid Build Coastguard Worker if self._locked: 1026*cda5da8dSAndroid Build Coastguard Worker f = open(os.path.join(self._path, str(key)), 'rb+') 1027*cda5da8dSAndroid Build Coastguard Worker else: 1028*cda5da8dSAndroid Build Coastguard Worker f = open(os.path.join(self._path, str(key)), 'rb') 1029*cda5da8dSAndroid Build Coastguard Worker except OSError as e: 1030*cda5da8dSAndroid Build Coastguard Worker if e.errno == errno.ENOENT: 1031*cda5da8dSAndroid Build Coastguard Worker raise KeyError('No message with key: %s' % key) 1032*cda5da8dSAndroid Build Coastguard Worker else: 1033*cda5da8dSAndroid Build Coastguard Worker raise 1034*cda5da8dSAndroid Build Coastguard Worker with f: 1035*cda5da8dSAndroid Build Coastguard Worker if self._locked: 1036*cda5da8dSAndroid Build Coastguard Worker _lock_file(f) 1037*cda5da8dSAndroid Build Coastguard Worker try: 1038*cda5da8dSAndroid Build Coastguard Worker msg = MHMessage(f) 1039*cda5da8dSAndroid Build Coastguard Worker finally: 1040*cda5da8dSAndroid Build Coastguard Worker if self._locked: 1041*cda5da8dSAndroid Build Coastguard Worker _unlock_file(f) 1042*cda5da8dSAndroid Build Coastguard Worker for name, key_list in self.get_sequences().items(): 1043*cda5da8dSAndroid Build Coastguard Worker if key in key_list: 1044*cda5da8dSAndroid Build Coastguard Worker msg.add_sequence(name) 1045*cda5da8dSAndroid Build Coastguard Worker return msg 1046*cda5da8dSAndroid Build Coastguard Worker 1047*cda5da8dSAndroid Build Coastguard Worker def get_bytes(self, key): 1048*cda5da8dSAndroid Build Coastguard Worker """Return a bytes representation or raise a KeyError.""" 1049*cda5da8dSAndroid Build Coastguard Worker try: 1050*cda5da8dSAndroid Build Coastguard Worker if self._locked: 1051*cda5da8dSAndroid Build Coastguard Worker f = open(os.path.join(self._path, str(key)), 'rb+') 1052*cda5da8dSAndroid Build Coastguard Worker else: 1053*cda5da8dSAndroid Build Coastguard Worker f = open(os.path.join(self._path, str(key)), 'rb') 1054*cda5da8dSAndroid Build Coastguard Worker except OSError as e: 1055*cda5da8dSAndroid Build Coastguard Worker if e.errno == errno.ENOENT: 1056*cda5da8dSAndroid Build Coastguard Worker raise KeyError('No message with key: %s' % key) 1057*cda5da8dSAndroid Build Coastguard Worker else: 1058*cda5da8dSAndroid Build Coastguard Worker raise 1059*cda5da8dSAndroid Build Coastguard Worker with f: 1060*cda5da8dSAndroid Build Coastguard Worker if self._locked: 1061*cda5da8dSAndroid Build Coastguard Worker _lock_file(f) 1062*cda5da8dSAndroid Build Coastguard Worker try: 1063*cda5da8dSAndroid Build Coastguard Worker return f.read().replace(linesep, b'\n') 1064*cda5da8dSAndroid Build Coastguard Worker finally: 1065*cda5da8dSAndroid Build Coastguard Worker if self._locked: 1066*cda5da8dSAndroid Build Coastguard Worker _unlock_file(f) 1067*cda5da8dSAndroid Build Coastguard Worker 1068*cda5da8dSAndroid Build Coastguard Worker def get_file(self, key): 1069*cda5da8dSAndroid Build Coastguard Worker """Return a file-like representation or raise a KeyError.""" 1070*cda5da8dSAndroid Build Coastguard Worker try: 1071*cda5da8dSAndroid Build Coastguard Worker f = open(os.path.join(self._path, str(key)), 'rb') 1072*cda5da8dSAndroid Build Coastguard Worker except OSError as e: 1073*cda5da8dSAndroid Build Coastguard Worker if e.errno == errno.ENOENT: 1074*cda5da8dSAndroid Build Coastguard Worker raise KeyError('No message with key: %s' % key) 1075*cda5da8dSAndroid Build Coastguard Worker else: 1076*cda5da8dSAndroid Build Coastguard Worker raise 1077*cda5da8dSAndroid Build Coastguard Worker return _ProxyFile(f) 1078*cda5da8dSAndroid Build Coastguard Worker 1079*cda5da8dSAndroid Build Coastguard Worker def iterkeys(self): 1080*cda5da8dSAndroid Build Coastguard Worker """Return an iterator over keys.""" 1081*cda5da8dSAndroid Build Coastguard Worker return iter(sorted(int(entry) for entry in os.listdir(self._path) 1082*cda5da8dSAndroid Build Coastguard Worker if entry.isdigit())) 1083*cda5da8dSAndroid Build Coastguard Worker 1084*cda5da8dSAndroid Build Coastguard Worker def __contains__(self, key): 1085*cda5da8dSAndroid Build Coastguard Worker """Return True if the keyed message exists, False otherwise.""" 1086*cda5da8dSAndroid Build Coastguard Worker return os.path.exists(os.path.join(self._path, str(key))) 1087*cda5da8dSAndroid Build Coastguard Worker 1088*cda5da8dSAndroid Build Coastguard Worker def __len__(self): 1089*cda5da8dSAndroid Build Coastguard Worker """Return a count of messages in the mailbox.""" 1090*cda5da8dSAndroid Build Coastguard Worker return len(list(self.iterkeys())) 1091*cda5da8dSAndroid Build Coastguard Worker 1092*cda5da8dSAndroid Build Coastguard Worker def lock(self): 1093*cda5da8dSAndroid Build Coastguard Worker """Lock the mailbox.""" 1094*cda5da8dSAndroid Build Coastguard Worker if not self._locked: 1095*cda5da8dSAndroid Build Coastguard Worker self._file = open(os.path.join(self._path, '.mh_sequences'), 'rb+') 1096*cda5da8dSAndroid Build Coastguard Worker _lock_file(self._file) 1097*cda5da8dSAndroid Build Coastguard Worker self._locked = True 1098*cda5da8dSAndroid Build Coastguard Worker 1099*cda5da8dSAndroid Build Coastguard Worker def unlock(self): 1100*cda5da8dSAndroid Build Coastguard Worker """Unlock the mailbox if it is locked.""" 1101*cda5da8dSAndroid Build Coastguard Worker if self._locked: 1102*cda5da8dSAndroid Build Coastguard Worker _unlock_file(self._file) 1103*cda5da8dSAndroid Build Coastguard Worker _sync_close(self._file) 1104*cda5da8dSAndroid Build Coastguard Worker del self._file 1105*cda5da8dSAndroid Build Coastguard Worker self._locked = False 1106*cda5da8dSAndroid Build Coastguard Worker 1107*cda5da8dSAndroid Build Coastguard Worker def flush(self): 1108*cda5da8dSAndroid Build Coastguard Worker """Write any pending changes to the disk.""" 1109*cda5da8dSAndroid Build Coastguard Worker return 1110*cda5da8dSAndroid Build Coastguard Worker 1111*cda5da8dSAndroid Build Coastguard Worker def close(self): 1112*cda5da8dSAndroid Build Coastguard Worker """Flush and close the mailbox.""" 1113*cda5da8dSAndroid Build Coastguard Worker if self._locked: 1114*cda5da8dSAndroid Build Coastguard Worker self.unlock() 1115*cda5da8dSAndroid Build Coastguard Worker 1116*cda5da8dSAndroid Build Coastguard Worker def list_folders(self): 1117*cda5da8dSAndroid Build Coastguard Worker """Return a list of folder names.""" 1118*cda5da8dSAndroid Build Coastguard Worker result = [] 1119*cda5da8dSAndroid Build Coastguard Worker for entry in os.listdir(self._path): 1120*cda5da8dSAndroid Build Coastguard Worker if os.path.isdir(os.path.join(self._path, entry)): 1121*cda5da8dSAndroid Build Coastguard Worker result.append(entry) 1122*cda5da8dSAndroid Build Coastguard Worker return result 1123*cda5da8dSAndroid Build Coastguard Worker 1124*cda5da8dSAndroid Build Coastguard Worker def get_folder(self, folder): 1125*cda5da8dSAndroid Build Coastguard Worker """Return an MH instance for the named folder.""" 1126*cda5da8dSAndroid Build Coastguard Worker return MH(os.path.join(self._path, folder), 1127*cda5da8dSAndroid Build Coastguard Worker factory=self._factory, create=False) 1128*cda5da8dSAndroid Build Coastguard Worker 1129*cda5da8dSAndroid Build Coastguard Worker def add_folder(self, folder): 1130*cda5da8dSAndroid Build Coastguard Worker """Create a folder and return an MH instance representing it.""" 1131*cda5da8dSAndroid Build Coastguard Worker return MH(os.path.join(self._path, folder), 1132*cda5da8dSAndroid Build Coastguard Worker factory=self._factory) 1133*cda5da8dSAndroid Build Coastguard Worker 1134*cda5da8dSAndroid Build Coastguard Worker def remove_folder(self, folder): 1135*cda5da8dSAndroid Build Coastguard Worker """Delete the named folder, which must be empty.""" 1136*cda5da8dSAndroid Build Coastguard Worker path = os.path.join(self._path, folder) 1137*cda5da8dSAndroid Build Coastguard Worker entries = os.listdir(path) 1138*cda5da8dSAndroid Build Coastguard Worker if entries == ['.mh_sequences']: 1139*cda5da8dSAndroid Build Coastguard Worker os.remove(os.path.join(path, '.mh_sequences')) 1140*cda5da8dSAndroid Build Coastguard Worker elif entries == []: 1141*cda5da8dSAndroid Build Coastguard Worker pass 1142*cda5da8dSAndroid Build Coastguard Worker else: 1143*cda5da8dSAndroid Build Coastguard Worker raise NotEmptyError('Folder not empty: %s' % self._path) 1144*cda5da8dSAndroid Build Coastguard Worker os.rmdir(path) 1145*cda5da8dSAndroid Build Coastguard Worker 1146*cda5da8dSAndroid Build Coastguard Worker def get_sequences(self): 1147*cda5da8dSAndroid Build Coastguard Worker """Return a name-to-key-list dictionary to define each sequence.""" 1148*cda5da8dSAndroid Build Coastguard Worker results = {} 1149*cda5da8dSAndroid Build Coastguard Worker with open(os.path.join(self._path, '.mh_sequences'), 'r', encoding='ASCII') as f: 1150*cda5da8dSAndroid Build Coastguard Worker all_keys = set(self.keys()) 1151*cda5da8dSAndroid Build Coastguard Worker for line in f: 1152*cda5da8dSAndroid Build Coastguard Worker try: 1153*cda5da8dSAndroid Build Coastguard Worker name, contents = line.split(':') 1154*cda5da8dSAndroid Build Coastguard Worker keys = set() 1155*cda5da8dSAndroid Build Coastguard Worker for spec in contents.split(): 1156*cda5da8dSAndroid Build Coastguard Worker if spec.isdigit(): 1157*cda5da8dSAndroid Build Coastguard Worker keys.add(int(spec)) 1158*cda5da8dSAndroid Build Coastguard Worker else: 1159*cda5da8dSAndroid Build Coastguard Worker start, stop = (int(x) for x in spec.split('-')) 1160*cda5da8dSAndroid Build Coastguard Worker keys.update(range(start, stop + 1)) 1161*cda5da8dSAndroid Build Coastguard Worker results[name] = [key for key in sorted(keys) \ 1162*cda5da8dSAndroid Build Coastguard Worker if key in all_keys] 1163*cda5da8dSAndroid Build Coastguard Worker if len(results[name]) == 0: 1164*cda5da8dSAndroid Build Coastguard Worker del results[name] 1165*cda5da8dSAndroid Build Coastguard Worker except ValueError: 1166*cda5da8dSAndroid Build Coastguard Worker raise FormatError('Invalid sequence specification: %s' % 1167*cda5da8dSAndroid Build Coastguard Worker line.rstrip()) 1168*cda5da8dSAndroid Build Coastguard Worker return results 1169*cda5da8dSAndroid Build Coastguard Worker 1170*cda5da8dSAndroid Build Coastguard Worker def set_sequences(self, sequences): 1171*cda5da8dSAndroid Build Coastguard Worker """Set sequences using the given name-to-key-list dictionary.""" 1172*cda5da8dSAndroid Build Coastguard Worker f = open(os.path.join(self._path, '.mh_sequences'), 'r+', encoding='ASCII') 1173*cda5da8dSAndroid Build Coastguard Worker try: 1174*cda5da8dSAndroid Build Coastguard Worker os.close(os.open(f.name, os.O_WRONLY | os.O_TRUNC)) 1175*cda5da8dSAndroid Build Coastguard Worker for name, keys in sequences.items(): 1176*cda5da8dSAndroid Build Coastguard Worker if len(keys) == 0: 1177*cda5da8dSAndroid Build Coastguard Worker continue 1178*cda5da8dSAndroid Build Coastguard Worker f.write(name + ':') 1179*cda5da8dSAndroid Build Coastguard Worker prev = None 1180*cda5da8dSAndroid Build Coastguard Worker completing = False 1181*cda5da8dSAndroid Build Coastguard Worker for key in sorted(set(keys)): 1182*cda5da8dSAndroid Build Coastguard Worker if key - 1 == prev: 1183*cda5da8dSAndroid Build Coastguard Worker if not completing: 1184*cda5da8dSAndroid Build Coastguard Worker completing = True 1185*cda5da8dSAndroid Build Coastguard Worker f.write('-') 1186*cda5da8dSAndroid Build Coastguard Worker elif completing: 1187*cda5da8dSAndroid Build Coastguard Worker completing = False 1188*cda5da8dSAndroid Build Coastguard Worker f.write('%s %s' % (prev, key)) 1189*cda5da8dSAndroid Build Coastguard Worker else: 1190*cda5da8dSAndroid Build Coastguard Worker f.write(' %s' % key) 1191*cda5da8dSAndroid Build Coastguard Worker prev = key 1192*cda5da8dSAndroid Build Coastguard Worker if completing: 1193*cda5da8dSAndroid Build Coastguard Worker f.write(str(prev) + '\n') 1194*cda5da8dSAndroid Build Coastguard Worker else: 1195*cda5da8dSAndroid Build Coastguard Worker f.write('\n') 1196*cda5da8dSAndroid Build Coastguard Worker finally: 1197*cda5da8dSAndroid Build Coastguard Worker _sync_close(f) 1198*cda5da8dSAndroid Build Coastguard Worker 1199*cda5da8dSAndroid Build Coastguard Worker def pack(self): 1200*cda5da8dSAndroid Build Coastguard Worker """Re-name messages to eliminate numbering gaps. Invalidates keys.""" 1201*cda5da8dSAndroid Build Coastguard Worker sequences = self.get_sequences() 1202*cda5da8dSAndroid Build Coastguard Worker prev = 0 1203*cda5da8dSAndroid Build Coastguard Worker changes = [] 1204*cda5da8dSAndroid Build Coastguard Worker for key in self.iterkeys(): 1205*cda5da8dSAndroid Build Coastguard Worker if key - 1 != prev: 1206*cda5da8dSAndroid Build Coastguard Worker changes.append((key, prev + 1)) 1207*cda5da8dSAndroid Build Coastguard Worker try: 1208*cda5da8dSAndroid Build Coastguard Worker os.link(os.path.join(self._path, str(key)), 1209*cda5da8dSAndroid Build Coastguard Worker os.path.join(self._path, str(prev + 1))) 1210*cda5da8dSAndroid Build Coastguard Worker except (AttributeError, PermissionError): 1211*cda5da8dSAndroid Build Coastguard Worker os.rename(os.path.join(self._path, str(key)), 1212*cda5da8dSAndroid Build Coastguard Worker os.path.join(self._path, str(prev + 1))) 1213*cda5da8dSAndroid Build Coastguard Worker else: 1214*cda5da8dSAndroid Build Coastguard Worker os.unlink(os.path.join(self._path, str(key))) 1215*cda5da8dSAndroid Build Coastguard Worker prev += 1 1216*cda5da8dSAndroid Build Coastguard Worker self._next_key = prev + 1 1217*cda5da8dSAndroid Build Coastguard Worker if len(changes) == 0: 1218*cda5da8dSAndroid Build Coastguard Worker return 1219*cda5da8dSAndroid Build Coastguard Worker for name, key_list in sequences.items(): 1220*cda5da8dSAndroid Build Coastguard Worker for old, new in changes: 1221*cda5da8dSAndroid Build Coastguard Worker if old in key_list: 1222*cda5da8dSAndroid Build Coastguard Worker key_list[key_list.index(old)] = new 1223*cda5da8dSAndroid Build Coastguard Worker self.set_sequences(sequences) 1224*cda5da8dSAndroid Build Coastguard Worker 1225*cda5da8dSAndroid Build Coastguard Worker def _dump_sequences(self, message, key): 1226*cda5da8dSAndroid Build Coastguard Worker """Inspect a new MHMessage and update sequences appropriately.""" 1227*cda5da8dSAndroid Build Coastguard Worker pending_sequences = message.get_sequences() 1228*cda5da8dSAndroid Build Coastguard Worker all_sequences = self.get_sequences() 1229*cda5da8dSAndroid Build Coastguard Worker for name, key_list in all_sequences.items(): 1230*cda5da8dSAndroid Build Coastguard Worker if name in pending_sequences: 1231*cda5da8dSAndroid Build Coastguard Worker key_list.append(key) 1232*cda5da8dSAndroid Build Coastguard Worker elif key in key_list: 1233*cda5da8dSAndroid Build Coastguard Worker del key_list[key_list.index(key)] 1234*cda5da8dSAndroid Build Coastguard Worker for sequence in pending_sequences: 1235*cda5da8dSAndroid Build Coastguard Worker if sequence not in all_sequences: 1236*cda5da8dSAndroid Build Coastguard Worker all_sequences[sequence] = [key] 1237*cda5da8dSAndroid Build Coastguard Worker self.set_sequences(all_sequences) 1238*cda5da8dSAndroid Build Coastguard Worker 1239*cda5da8dSAndroid Build Coastguard Worker 1240*cda5da8dSAndroid Build Coastguard Workerclass Babyl(_singlefileMailbox): 1241*cda5da8dSAndroid Build Coastguard Worker """An Rmail-style Babyl mailbox.""" 1242*cda5da8dSAndroid Build Coastguard Worker 1243*cda5da8dSAndroid Build Coastguard Worker _special_labels = frozenset({'unseen', 'deleted', 'filed', 'answered', 1244*cda5da8dSAndroid Build Coastguard Worker 'forwarded', 'edited', 'resent'}) 1245*cda5da8dSAndroid Build Coastguard Worker 1246*cda5da8dSAndroid Build Coastguard Worker def __init__(self, path, factory=None, create=True): 1247*cda5da8dSAndroid Build Coastguard Worker """Initialize a Babyl mailbox.""" 1248*cda5da8dSAndroid Build Coastguard Worker _singlefileMailbox.__init__(self, path, factory, create) 1249*cda5da8dSAndroid Build Coastguard Worker self._labels = {} 1250*cda5da8dSAndroid Build Coastguard Worker 1251*cda5da8dSAndroid Build Coastguard Worker def add(self, message): 1252*cda5da8dSAndroid Build Coastguard Worker """Add message and return assigned key.""" 1253*cda5da8dSAndroid Build Coastguard Worker key = _singlefileMailbox.add(self, message) 1254*cda5da8dSAndroid Build Coastguard Worker if isinstance(message, BabylMessage): 1255*cda5da8dSAndroid Build Coastguard Worker self._labels[key] = message.get_labels() 1256*cda5da8dSAndroid Build Coastguard Worker return key 1257*cda5da8dSAndroid Build Coastguard Worker 1258*cda5da8dSAndroid Build Coastguard Worker def remove(self, key): 1259*cda5da8dSAndroid Build Coastguard Worker """Remove the keyed message; raise KeyError if it doesn't exist.""" 1260*cda5da8dSAndroid Build Coastguard Worker _singlefileMailbox.remove(self, key) 1261*cda5da8dSAndroid Build Coastguard Worker if key in self._labels: 1262*cda5da8dSAndroid Build Coastguard Worker del self._labels[key] 1263*cda5da8dSAndroid Build Coastguard Worker 1264*cda5da8dSAndroid Build Coastguard Worker def __setitem__(self, key, message): 1265*cda5da8dSAndroid Build Coastguard Worker """Replace the keyed message; raise KeyError if it doesn't exist.""" 1266*cda5da8dSAndroid Build Coastguard Worker _singlefileMailbox.__setitem__(self, key, message) 1267*cda5da8dSAndroid Build Coastguard Worker if isinstance(message, BabylMessage): 1268*cda5da8dSAndroid Build Coastguard Worker self._labels[key] = message.get_labels() 1269*cda5da8dSAndroid Build Coastguard Worker 1270*cda5da8dSAndroid Build Coastguard Worker def get_message(self, key): 1271*cda5da8dSAndroid Build Coastguard Worker """Return a Message representation or raise a KeyError.""" 1272*cda5da8dSAndroid Build Coastguard Worker start, stop = self._lookup(key) 1273*cda5da8dSAndroid Build Coastguard Worker self._file.seek(start) 1274*cda5da8dSAndroid Build Coastguard Worker self._file.readline() # Skip b'1,' line specifying labels. 1275*cda5da8dSAndroid Build Coastguard Worker original_headers = io.BytesIO() 1276*cda5da8dSAndroid Build Coastguard Worker while True: 1277*cda5da8dSAndroid Build Coastguard Worker line = self._file.readline() 1278*cda5da8dSAndroid Build Coastguard Worker if line == b'*** EOOH ***' + linesep or not line: 1279*cda5da8dSAndroid Build Coastguard Worker break 1280*cda5da8dSAndroid Build Coastguard Worker original_headers.write(line.replace(linesep, b'\n')) 1281*cda5da8dSAndroid Build Coastguard Worker visible_headers = io.BytesIO() 1282*cda5da8dSAndroid Build Coastguard Worker while True: 1283*cda5da8dSAndroid Build Coastguard Worker line = self._file.readline() 1284*cda5da8dSAndroid Build Coastguard Worker if line == linesep or not line: 1285*cda5da8dSAndroid Build Coastguard Worker break 1286*cda5da8dSAndroid Build Coastguard Worker visible_headers.write(line.replace(linesep, b'\n')) 1287*cda5da8dSAndroid Build Coastguard Worker # Read up to the stop, or to the end 1288*cda5da8dSAndroid Build Coastguard Worker n = stop - self._file.tell() 1289*cda5da8dSAndroid Build Coastguard Worker assert n >= 0 1290*cda5da8dSAndroid Build Coastguard Worker body = self._file.read(n) 1291*cda5da8dSAndroid Build Coastguard Worker body = body.replace(linesep, b'\n') 1292*cda5da8dSAndroid Build Coastguard Worker msg = BabylMessage(original_headers.getvalue() + body) 1293*cda5da8dSAndroid Build Coastguard Worker msg.set_visible(visible_headers.getvalue()) 1294*cda5da8dSAndroid Build Coastguard Worker if key in self._labels: 1295*cda5da8dSAndroid Build Coastguard Worker msg.set_labels(self._labels[key]) 1296*cda5da8dSAndroid Build Coastguard Worker return msg 1297*cda5da8dSAndroid Build Coastguard Worker 1298*cda5da8dSAndroid Build Coastguard Worker def get_bytes(self, key): 1299*cda5da8dSAndroid Build Coastguard Worker """Return a string representation or raise a KeyError.""" 1300*cda5da8dSAndroid Build Coastguard Worker start, stop = self._lookup(key) 1301*cda5da8dSAndroid Build Coastguard Worker self._file.seek(start) 1302*cda5da8dSAndroid Build Coastguard Worker self._file.readline() # Skip b'1,' line specifying labels. 1303*cda5da8dSAndroid Build Coastguard Worker original_headers = io.BytesIO() 1304*cda5da8dSAndroid Build Coastguard Worker while True: 1305*cda5da8dSAndroid Build Coastguard Worker line = self._file.readline() 1306*cda5da8dSAndroid Build Coastguard Worker if line == b'*** EOOH ***' + linesep or not line: 1307*cda5da8dSAndroid Build Coastguard Worker break 1308*cda5da8dSAndroid Build Coastguard Worker original_headers.write(line.replace(linesep, b'\n')) 1309*cda5da8dSAndroid Build Coastguard Worker while True: 1310*cda5da8dSAndroid Build Coastguard Worker line = self._file.readline() 1311*cda5da8dSAndroid Build Coastguard Worker if line == linesep or not line: 1312*cda5da8dSAndroid Build Coastguard Worker break 1313*cda5da8dSAndroid Build Coastguard Worker headers = original_headers.getvalue() 1314*cda5da8dSAndroid Build Coastguard Worker n = stop - self._file.tell() 1315*cda5da8dSAndroid Build Coastguard Worker assert n >= 0 1316*cda5da8dSAndroid Build Coastguard Worker data = self._file.read(n) 1317*cda5da8dSAndroid Build Coastguard Worker data = data.replace(linesep, b'\n') 1318*cda5da8dSAndroid Build Coastguard Worker return headers + data 1319*cda5da8dSAndroid Build Coastguard Worker 1320*cda5da8dSAndroid Build Coastguard Worker def get_file(self, key): 1321*cda5da8dSAndroid Build Coastguard Worker """Return a file-like representation or raise a KeyError.""" 1322*cda5da8dSAndroid Build Coastguard Worker return io.BytesIO(self.get_bytes(key).replace(b'\n', linesep)) 1323*cda5da8dSAndroid Build Coastguard Worker 1324*cda5da8dSAndroid Build Coastguard Worker def get_labels(self): 1325*cda5da8dSAndroid Build Coastguard Worker """Return a list of user-defined labels in the mailbox.""" 1326*cda5da8dSAndroid Build Coastguard Worker self._lookup() 1327*cda5da8dSAndroid Build Coastguard Worker labels = set() 1328*cda5da8dSAndroid Build Coastguard Worker for label_list in self._labels.values(): 1329*cda5da8dSAndroid Build Coastguard Worker labels.update(label_list) 1330*cda5da8dSAndroid Build Coastguard Worker labels.difference_update(self._special_labels) 1331*cda5da8dSAndroid Build Coastguard Worker return list(labels) 1332*cda5da8dSAndroid Build Coastguard Worker 1333*cda5da8dSAndroid Build Coastguard Worker def _generate_toc(self): 1334*cda5da8dSAndroid Build Coastguard Worker """Generate key-to-(start, stop) table of contents.""" 1335*cda5da8dSAndroid Build Coastguard Worker starts, stops = [], [] 1336*cda5da8dSAndroid Build Coastguard Worker self._file.seek(0) 1337*cda5da8dSAndroid Build Coastguard Worker next_pos = 0 1338*cda5da8dSAndroid Build Coastguard Worker label_lists = [] 1339*cda5da8dSAndroid Build Coastguard Worker while True: 1340*cda5da8dSAndroid Build Coastguard Worker line_pos = next_pos 1341*cda5da8dSAndroid Build Coastguard Worker line = self._file.readline() 1342*cda5da8dSAndroid Build Coastguard Worker next_pos = self._file.tell() 1343*cda5da8dSAndroid Build Coastguard Worker if line == b'\037\014' + linesep: 1344*cda5da8dSAndroid Build Coastguard Worker if len(stops) < len(starts): 1345*cda5da8dSAndroid Build Coastguard Worker stops.append(line_pos - len(linesep)) 1346*cda5da8dSAndroid Build Coastguard Worker starts.append(next_pos) 1347*cda5da8dSAndroid Build Coastguard Worker labels = [label.strip() for label 1348*cda5da8dSAndroid Build Coastguard Worker in self._file.readline()[1:].split(b',') 1349*cda5da8dSAndroid Build Coastguard Worker if label.strip()] 1350*cda5da8dSAndroid Build Coastguard Worker label_lists.append(labels) 1351*cda5da8dSAndroid Build Coastguard Worker elif line == b'\037' or line == b'\037' + linesep: 1352*cda5da8dSAndroid Build Coastguard Worker if len(stops) < len(starts): 1353*cda5da8dSAndroid Build Coastguard Worker stops.append(line_pos - len(linesep)) 1354*cda5da8dSAndroid Build Coastguard Worker elif not line: 1355*cda5da8dSAndroid Build Coastguard Worker stops.append(line_pos - len(linesep)) 1356*cda5da8dSAndroid Build Coastguard Worker break 1357*cda5da8dSAndroid Build Coastguard Worker self._toc = dict(enumerate(zip(starts, stops))) 1358*cda5da8dSAndroid Build Coastguard Worker self._labels = dict(enumerate(label_lists)) 1359*cda5da8dSAndroid Build Coastguard Worker self._next_key = len(self._toc) 1360*cda5da8dSAndroid Build Coastguard Worker self._file.seek(0, 2) 1361*cda5da8dSAndroid Build Coastguard Worker self._file_length = self._file.tell() 1362*cda5da8dSAndroid Build Coastguard Worker 1363*cda5da8dSAndroid Build Coastguard Worker def _pre_mailbox_hook(self, f): 1364*cda5da8dSAndroid Build Coastguard Worker """Called before writing the mailbox to file f.""" 1365*cda5da8dSAndroid Build Coastguard Worker babyl = b'BABYL OPTIONS:' + linesep 1366*cda5da8dSAndroid Build Coastguard Worker babyl += b'Version: 5' + linesep 1367*cda5da8dSAndroid Build Coastguard Worker labels = self.get_labels() 1368*cda5da8dSAndroid Build Coastguard Worker labels = (label.encode() for label in labels) 1369*cda5da8dSAndroid Build Coastguard Worker babyl += b'Labels:' + b','.join(labels) + linesep 1370*cda5da8dSAndroid Build Coastguard Worker babyl += b'\037' 1371*cda5da8dSAndroid Build Coastguard Worker f.write(babyl) 1372*cda5da8dSAndroid Build Coastguard Worker 1373*cda5da8dSAndroid Build Coastguard Worker def _pre_message_hook(self, f): 1374*cda5da8dSAndroid Build Coastguard Worker """Called before writing each message to file f.""" 1375*cda5da8dSAndroid Build Coastguard Worker f.write(b'\014' + linesep) 1376*cda5da8dSAndroid Build Coastguard Worker 1377*cda5da8dSAndroid Build Coastguard Worker def _post_message_hook(self, f): 1378*cda5da8dSAndroid Build Coastguard Worker """Called after writing each message to file f.""" 1379*cda5da8dSAndroid Build Coastguard Worker f.write(linesep + b'\037') 1380*cda5da8dSAndroid Build Coastguard Worker 1381*cda5da8dSAndroid Build Coastguard Worker def _install_message(self, message): 1382*cda5da8dSAndroid Build Coastguard Worker """Write message contents and return (start, stop).""" 1383*cda5da8dSAndroid Build Coastguard Worker start = self._file.tell() 1384*cda5da8dSAndroid Build Coastguard Worker if isinstance(message, BabylMessage): 1385*cda5da8dSAndroid Build Coastguard Worker special_labels = [] 1386*cda5da8dSAndroid Build Coastguard Worker labels = [] 1387*cda5da8dSAndroid Build Coastguard Worker for label in message.get_labels(): 1388*cda5da8dSAndroid Build Coastguard Worker if label in self._special_labels: 1389*cda5da8dSAndroid Build Coastguard Worker special_labels.append(label) 1390*cda5da8dSAndroid Build Coastguard Worker else: 1391*cda5da8dSAndroid Build Coastguard Worker labels.append(label) 1392*cda5da8dSAndroid Build Coastguard Worker self._file.write(b'1') 1393*cda5da8dSAndroid Build Coastguard Worker for label in special_labels: 1394*cda5da8dSAndroid Build Coastguard Worker self._file.write(b', ' + label.encode()) 1395*cda5da8dSAndroid Build Coastguard Worker self._file.write(b',,') 1396*cda5da8dSAndroid Build Coastguard Worker for label in labels: 1397*cda5da8dSAndroid Build Coastguard Worker self._file.write(b' ' + label.encode() + b',') 1398*cda5da8dSAndroid Build Coastguard Worker self._file.write(linesep) 1399*cda5da8dSAndroid Build Coastguard Worker else: 1400*cda5da8dSAndroid Build Coastguard Worker self._file.write(b'1,,' + linesep) 1401*cda5da8dSAndroid Build Coastguard Worker if isinstance(message, email.message.Message): 1402*cda5da8dSAndroid Build Coastguard Worker orig_buffer = io.BytesIO() 1403*cda5da8dSAndroid Build Coastguard Worker orig_generator = email.generator.BytesGenerator(orig_buffer, False, 0) 1404*cda5da8dSAndroid Build Coastguard Worker orig_generator.flatten(message) 1405*cda5da8dSAndroid Build Coastguard Worker orig_buffer.seek(0) 1406*cda5da8dSAndroid Build Coastguard Worker while True: 1407*cda5da8dSAndroid Build Coastguard Worker line = orig_buffer.readline() 1408*cda5da8dSAndroid Build Coastguard Worker self._file.write(line.replace(b'\n', linesep)) 1409*cda5da8dSAndroid Build Coastguard Worker if line == b'\n' or not line: 1410*cda5da8dSAndroid Build Coastguard Worker break 1411*cda5da8dSAndroid Build Coastguard Worker self._file.write(b'*** EOOH ***' + linesep) 1412*cda5da8dSAndroid Build Coastguard Worker if isinstance(message, BabylMessage): 1413*cda5da8dSAndroid Build Coastguard Worker vis_buffer = io.BytesIO() 1414*cda5da8dSAndroid Build Coastguard Worker vis_generator = email.generator.BytesGenerator(vis_buffer, False, 0) 1415*cda5da8dSAndroid Build Coastguard Worker vis_generator.flatten(message.get_visible()) 1416*cda5da8dSAndroid Build Coastguard Worker while True: 1417*cda5da8dSAndroid Build Coastguard Worker line = vis_buffer.readline() 1418*cda5da8dSAndroid Build Coastguard Worker self._file.write(line.replace(b'\n', linesep)) 1419*cda5da8dSAndroid Build Coastguard Worker if line == b'\n' or not line: 1420*cda5da8dSAndroid Build Coastguard Worker break 1421*cda5da8dSAndroid Build Coastguard Worker else: 1422*cda5da8dSAndroid Build Coastguard Worker orig_buffer.seek(0) 1423*cda5da8dSAndroid Build Coastguard Worker while True: 1424*cda5da8dSAndroid Build Coastguard Worker line = orig_buffer.readline() 1425*cda5da8dSAndroid Build Coastguard Worker self._file.write(line.replace(b'\n', linesep)) 1426*cda5da8dSAndroid Build Coastguard Worker if line == b'\n' or not line: 1427*cda5da8dSAndroid Build Coastguard Worker break 1428*cda5da8dSAndroid Build Coastguard Worker while True: 1429*cda5da8dSAndroid Build Coastguard Worker buffer = orig_buffer.read(4096) # Buffer size is arbitrary. 1430*cda5da8dSAndroid Build Coastguard Worker if not buffer: 1431*cda5da8dSAndroid Build Coastguard Worker break 1432*cda5da8dSAndroid Build Coastguard Worker self._file.write(buffer.replace(b'\n', linesep)) 1433*cda5da8dSAndroid Build Coastguard Worker elif isinstance(message, (bytes, str, io.StringIO)): 1434*cda5da8dSAndroid Build Coastguard Worker if isinstance(message, io.StringIO): 1435*cda5da8dSAndroid Build Coastguard Worker warnings.warn("Use of StringIO input is deprecated, " 1436*cda5da8dSAndroid Build Coastguard Worker "use BytesIO instead", DeprecationWarning, 3) 1437*cda5da8dSAndroid Build Coastguard Worker message = message.getvalue() 1438*cda5da8dSAndroid Build Coastguard Worker if isinstance(message, str): 1439*cda5da8dSAndroid Build Coastguard Worker message = self._string_to_bytes(message) 1440*cda5da8dSAndroid Build Coastguard Worker body_start = message.find(b'\n\n') + 2 1441*cda5da8dSAndroid Build Coastguard Worker if body_start - 2 != -1: 1442*cda5da8dSAndroid Build Coastguard Worker self._file.write(message[:body_start].replace(b'\n', linesep)) 1443*cda5da8dSAndroid Build Coastguard Worker self._file.write(b'*** EOOH ***' + linesep) 1444*cda5da8dSAndroid Build Coastguard Worker self._file.write(message[:body_start].replace(b'\n', linesep)) 1445*cda5da8dSAndroid Build Coastguard Worker self._file.write(message[body_start:].replace(b'\n', linesep)) 1446*cda5da8dSAndroid Build Coastguard Worker else: 1447*cda5da8dSAndroid Build Coastguard Worker self._file.write(b'*** EOOH ***' + linesep + linesep) 1448*cda5da8dSAndroid Build Coastguard Worker self._file.write(message.replace(b'\n', linesep)) 1449*cda5da8dSAndroid Build Coastguard Worker elif hasattr(message, 'readline'): 1450*cda5da8dSAndroid Build Coastguard Worker if hasattr(message, 'buffer'): 1451*cda5da8dSAndroid Build Coastguard Worker warnings.warn("Use of text mode files is deprecated, " 1452*cda5da8dSAndroid Build Coastguard Worker "use a binary mode file instead", DeprecationWarning, 3) 1453*cda5da8dSAndroid Build Coastguard Worker message = message.buffer 1454*cda5da8dSAndroid Build Coastguard Worker original_pos = message.tell() 1455*cda5da8dSAndroid Build Coastguard Worker first_pass = True 1456*cda5da8dSAndroid Build Coastguard Worker while True: 1457*cda5da8dSAndroid Build Coastguard Worker line = message.readline() 1458*cda5da8dSAndroid Build Coastguard Worker # Universal newline support. 1459*cda5da8dSAndroid Build Coastguard Worker if line.endswith(b'\r\n'): 1460*cda5da8dSAndroid Build Coastguard Worker line = line[:-2] + b'\n' 1461*cda5da8dSAndroid Build Coastguard Worker elif line.endswith(b'\r'): 1462*cda5da8dSAndroid Build Coastguard Worker line = line[:-1] + b'\n' 1463*cda5da8dSAndroid Build Coastguard Worker self._file.write(line.replace(b'\n', linesep)) 1464*cda5da8dSAndroid Build Coastguard Worker if line == b'\n' or not line: 1465*cda5da8dSAndroid Build Coastguard Worker if first_pass: 1466*cda5da8dSAndroid Build Coastguard Worker first_pass = False 1467*cda5da8dSAndroid Build Coastguard Worker self._file.write(b'*** EOOH ***' + linesep) 1468*cda5da8dSAndroid Build Coastguard Worker message.seek(original_pos) 1469*cda5da8dSAndroid Build Coastguard Worker else: 1470*cda5da8dSAndroid Build Coastguard Worker break 1471*cda5da8dSAndroid Build Coastguard Worker while True: 1472*cda5da8dSAndroid Build Coastguard Worker line = message.readline() 1473*cda5da8dSAndroid Build Coastguard Worker if not line: 1474*cda5da8dSAndroid Build Coastguard Worker break 1475*cda5da8dSAndroid Build Coastguard Worker # Universal newline support. 1476*cda5da8dSAndroid Build Coastguard Worker if line.endswith(b'\r\n'): 1477*cda5da8dSAndroid Build Coastguard Worker line = line[:-2] + linesep 1478*cda5da8dSAndroid Build Coastguard Worker elif line.endswith(b'\r'): 1479*cda5da8dSAndroid Build Coastguard Worker line = line[:-1] + linesep 1480*cda5da8dSAndroid Build Coastguard Worker elif line.endswith(b'\n'): 1481*cda5da8dSAndroid Build Coastguard Worker line = line[:-1] + linesep 1482*cda5da8dSAndroid Build Coastguard Worker self._file.write(line) 1483*cda5da8dSAndroid Build Coastguard Worker else: 1484*cda5da8dSAndroid Build Coastguard Worker raise TypeError('Invalid message type: %s' % type(message)) 1485*cda5da8dSAndroid Build Coastguard Worker stop = self._file.tell() 1486*cda5da8dSAndroid Build Coastguard Worker return (start, stop) 1487*cda5da8dSAndroid Build Coastguard Worker 1488*cda5da8dSAndroid Build Coastguard Worker 1489*cda5da8dSAndroid Build Coastguard Workerclass Message(email.message.Message): 1490*cda5da8dSAndroid Build Coastguard Worker """Message with mailbox-format-specific properties.""" 1491*cda5da8dSAndroid Build Coastguard Worker 1492*cda5da8dSAndroid Build Coastguard Worker def __init__(self, message=None): 1493*cda5da8dSAndroid Build Coastguard Worker """Initialize a Message instance.""" 1494*cda5da8dSAndroid Build Coastguard Worker if isinstance(message, email.message.Message): 1495*cda5da8dSAndroid Build Coastguard Worker self._become_message(copy.deepcopy(message)) 1496*cda5da8dSAndroid Build Coastguard Worker if isinstance(message, Message): 1497*cda5da8dSAndroid Build Coastguard Worker message._explain_to(self) 1498*cda5da8dSAndroid Build Coastguard Worker elif isinstance(message, bytes): 1499*cda5da8dSAndroid Build Coastguard Worker self._become_message(email.message_from_bytes(message)) 1500*cda5da8dSAndroid Build Coastguard Worker elif isinstance(message, str): 1501*cda5da8dSAndroid Build Coastguard Worker self._become_message(email.message_from_string(message)) 1502*cda5da8dSAndroid Build Coastguard Worker elif isinstance(message, io.TextIOWrapper): 1503*cda5da8dSAndroid Build Coastguard Worker self._become_message(email.message_from_file(message)) 1504*cda5da8dSAndroid Build Coastguard Worker elif hasattr(message, "read"): 1505*cda5da8dSAndroid Build Coastguard Worker self._become_message(email.message_from_binary_file(message)) 1506*cda5da8dSAndroid Build Coastguard Worker elif message is None: 1507*cda5da8dSAndroid Build Coastguard Worker email.message.Message.__init__(self) 1508*cda5da8dSAndroid Build Coastguard Worker else: 1509*cda5da8dSAndroid Build Coastguard Worker raise TypeError('Invalid message type: %s' % type(message)) 1510*cda5da8dSAndroid Build Coastguard Worker 1511*cda5da8dSAndroid Build Coastguard Worker def _become_message(self, message): 1512*cda5da8dSAndroid Build Coastguard Worker """Assume the non-format-specific state of message.""" 1513*cda5da8dSAndroid Build Coastguard Worker type_specific = getattr(message, '_type_specific_attributes', []) 1514*cda5da8dSAndroid Build Coastguard Worker for name in message.__dict__: 1515*cda5da8dSAndroid Build Coastguard Worker if name not in type_specific: 1516*cda5da8dSAndroid Build Coastguard Worker self.__dict__[name] = message.__dict__[name] 1517*cda5da8dSAndroid Build Coastguard Worker 1518*cda5da8dSAndroid Build Coastguard Worker def _explain_to(self, message): 1519*cda5da8dSAndroid Build Coastguard Worker """Copy format-specific state to message insofar as possible.""" 1520*cda5da8dSAndroid Build Coastguard Worker if isinstance(message, Message): 1521*cda5da8dSAndroid Build Coastguard Worker return # There's nothing format-specific to explain. 1522*cda5da8dSAndroid Build Coastguard Worker else: 1523*cda5da8dSAndroid Build Coastguard Worker raise TypeError('Cannot convert to specified type') 1524*cda5da8dSAndroid Build Coastguard Worker 1525*cda5da8dSAndroid Build Coastguard Worker 1526*cda5da8dSAndroid Build Coastguard Workerclass MaildirMessage(Message): 1527*cda5da8dSAndroid Build Coastguard Worker """Message with Maildir-specific properties.""" 1528*cda5da8dSAndroid Build Coastguard Worker 1529*cda5da8dSAndroid Build Coastguard Worker _type_specific_attributes = ['_subdir', '_info', '_date'] 1530*cda5da8dSAndroid Build Coastguard Worker 1531*cda5da8dSAndroid Build Coastguard Worker def __init__(self, message=None): 1532*cda5da8dSAndroid Build Coastguard Worker """Initialize a MaildirMessage instance.""" 1533*cda5da8dSAndroid Build Coastguard Worker self._subdir = 'new' 1534*cda5da8dSAndroid Build Coastguard Worker self._info = '' 1535*cda5da8dSAndroid Build Coastguard Worker self._date = time.time() 1536*cda5da8dSAndroid Build Coastguard Worker Message.__init__(self, message) 1537*cda5da8dSAndroid Build Coastguard Worker 1538*cda5da8dSAndroid Build Coastguard Worker def get_subdir(self): 1539*cda5da8dSAndroid Build Coastguard Worker """Return 'new' or 'cur'.""" 1540*cda5da8dSAndroid Build Coastguard Worker return self._subdir 1541*cda5da8dSAndroid Build Coastguard Worker 1542*cda5da8dSAndroid Build Coastguard Worker def set_subdir(self, subdir): 1543*cda5da8dSAndroid Build Coastguard Worker """Set subdir to 'new' or 'cur'.""" 1544*cda5da8dSAndroid Build Coastguard Worker if subdir == 'new' or subdir == 'cur': 1545*cda5da8dSAndroid Build Coastguard Worker self._subdir = subdir 1546*cda5da8dSAndroid Build Coastguard Worker else: 1547*cda5da8dSAndroid Build Coastguard Worker raise ValueError("subdir must be 'new' or 'cur': %s" % subdir) 1548*cda5da8dSAndroid Build Coastguard Worker 1549*cda5da8dSAndroid Build Coastguard Worker def get_flags(self): 1550*cda5da8dSAndroid Build Coastguard Worker """Return as a string the flags that are set.""" 1551*cda5da8dSAndroid Build Coastguard Worker if self._info.startswith('2,'): 1552*cda5da8dSAndroid Build Coastguard Worker return self._info[2:] 1553*cda5da8dSAndroid Build Coastguard Worker else: 1554*cda5da8dSAndroid Build Coastguard Worker return '' 1555*cda5da8dSAndroid Build Coastguard Worker 1556*cda5da8dSAndroid Build Coastguard Worker def set_flags(self, flags): 1557*cda5da8dSAndroid Build Coastguard Worker """Set the given flags and unset all others.""" 1558*cda5da8dSAndroid Build Coastguard Worker self._info = '2,' + ''.join(sorted(flags)) 1559*cda5da8dSAndroid Build Coastguard Worker 1560*cda5da8dSAndroid Build Coastguard Worker def add_flag(self, flag): 1561*cda5da8dSAndroid Build Coastguard Worker """Set the given flag(s) without changing others.""" 1562*cda5da8dSAndroid Build Coastguard Worker self.set_flags(''.join(set(self.get_flags()) | set(flag))) 1563*cda5da8dSAndroid Build Coastguard Worker 1564*cda5da8dSAndroid Build Coastguard Worker def remove_flag(self, flag): 1565*cda5da8dSAndroid Build Coastguard Worker """Unset the given string flag(s) without changing others.""" 1566*cda5da8dSAndroid Build Coastguard Worker if self.get_flags(): 1567*cda5da8dSAndroid Build Coastguard Worker self.set_flags(''.join(set(self.get_flags()) - set(flag))) 1568*cda5da8dSAndroid Build Coastguard Worker 1569*cda5da8dSAndroid Build Coastguard Worker def get_date(self): 1570*cda5da8dSAndroid Build Coastguard Worker """Return delivery date of message, in seconds since the epoch.""" 1571*cda5da8dSAndroid Build Coastguard Worker return self._date 1572*cda5da8dSAndroid Build Coastguard Worker 1573*cda5da8dSAndroid Build Coastguard Worker def set_date(self, date): 1574*cda5da8dSAndroid Build Coastguard Worker """Set delivery date of message, in seconds since the epoch.""" 1575*cda5da8dSAndroid Build Coastguard Worker try: 1576*cda5da8dSAndroid Build Coastguard Worker self._date = float(date) 1577*cda5da8dSAndroid Build Coastguard Worker except ValueError: 1578*cda5da8dSAndroid Build Coastguard Worker raise TypeError("can't convert to float: %s" % date) from None 1579*cda5da8dSAndroid Build Coastguard Worker 1580*cda5da8dSAndroid Build Coastguard Worker def get_info(self): 1581*cda5da8dSAndroid Build Coastguard Worker """Get the message's "info" as a string.""" 1582*cda5da8dSAndroid Build Coastguard Worker return self._info 1583*cda5da8dSAndroid Build Coastguard Worker 1584*cda5da8dSAndroid Build Coastguard Worker def set_info(self, info): 1585*cda5da8dSAndroid Build Coastguard Worker """Set the message's "info" string.""" 1586*cda5da8dSAndroid Build Coastguard Worker if isinstance(info, str): 1587*cda5da8dSAndroid Build Coastguard Worker self._info = info 1588*cda5da8dSAndroid Build Coastguard Worker else: 1589*cda5da8dSAndroid Build Coastguard Worker raise TypeError('info must be a string: %s' % type(info)) 1590*cda5da8dSAndroid Build Coastguard Worker 1591*cda5da8dSAndroid Build Coastguard Worker def _explain_to(self, message): 1592*cda5da8dSAndroid Build Coastguard Worker """Copy Maildir-specific state to message insofar as possible.""" 1593*cda5da8dSAndroid Build Coastguard Worker if isinstance(message, MaildirMessage): 1594*cda5da8dSAndroid Build Coastguard Worker message.set_flags(self.get_flags()) 1595*cda5da8dSAndroid Build Coastguard Worker message.set_subdir(self.get_subdir()) 1596*cda5da8dSAndroid Build Coastguard Worker message.set_date(self.get_date()) 1597*cda5da8dSAndroid Build Coastguard Worker elif isinstance(message, _mboxMMDFMessage): 1598*cda5da8dSAndroid Build Coastguard Worker flags = set(self.get_flags()) 1599*cda5da8dSAndroid Build Coastguard Worker if 'S' in flags: 1600*cda5da8dSAndroid Build Coastguard Worker message.add_flag('R') 1601*cda5da8dSAndroid Build Coastguard Worker if self.get_subdir() == 'cur': 1602*cda5da8dSAndroid Build Coastguard Worker message.add_flag('O') 1603*cda5da8dSAndroid Build Coastguard Worker if 'T' in flags: 1604*cda5da8dSAndroid Build Coastguard Worker message.add_flag('D') 1605*cda5da8dSAndroid Build Coastguard Worker if 'F' in flags: 1606*cda5da8dSAndroid Build Coastguard Worker message.add_flag('F') 1607*cda5da8dSAndroid Build Coastguard Worker if 'R' in flags: 1608*cda5da8dSAndroid Build Coastguard Worker message.add_flag('A') 1609*cda5da8dSAndroid Build Coastguard Worker message.set_from('MAILER-DAEMON', time.gmtime(self.get_date())) 1610*cda5da8dSAndroid Build Coastguard Worker elif isinstance(message, MHMessage): 1611*cda5da8dSAndroid Build Coastguard Worker flags = set(self.get_flags()) 1612*cda5da8dSAndroid Build Coastguard Worker if 'S' not in flags: 1613*cda5da8dSAndroid Build Coastguard Worker message.add_sequence('unseen') 1614*cda5da8dSAndroid Build Coastguard Worker if 'R' in flags: 1615*cda5da8dSAndroid Build Coastguard Worker message.add_sequence('replied') 1616*cda5da8dSAndroid Build Coastguard Worker if 'F' in flags: 1617*cda5da8dSAndroid Build Coastguard Worker message.add_sequence('flagged') 1618*cda5da8dSAndroid Build Coastguard Worker elif isinstance(message, BabylMessage): 1619*cda5da8dSAndroid Build Coastguard Worker flags = set(self.get_flags()) 1620*cda5da8dSAndroid Build Coastguard Worker if 'S' not in flags: 1621*cda5da8dSAndroid Build Coastguard Worker message.add_label('unseen') 1622*cda5da8dSAndroid Build Coastguard Worker if 'T' in flags: 1623*cda5da8dSAndroid Build Coastguard Worker message.add_label('deleted') 1624*cda5da8dSAndroid Build Coastguard Worker if 'R' in flags: 1625*cda5da8dSAndroid Build Coastguard Worker message.add_label('answered') 1626*cda5da8dSAndroid Build Coastguard Worker if 'P' in flags: 1627*cda5da8dSAndroid Build Coastguard Worker message.add_label('forwarded') 1628*cda5da8dSAndroid Build Coastguard Worker elif isinstance(message, Message): 1629*cda5da8dSAndroid Build Coastguard Worker pass 1630*cda5da8dSAndroid Build Coastguard Worker else: 1631*cda5da8dSAndroid Build Coastguard Worker raise TypeError('Cannot convert to specified type: %s' % 1632*cda5da8dSAndroid Build Coastguard Worker type(message)) 1633*cda5da8dSAndroid Build Coastguard Worker 1634*cda5da8dSAndroid Build Coastguard Worker 1635*cda5da8dSAndroid Build Coastguard Workerclass _mboxMMDFMessage(Message): 1636*cda5da8dSAndroid Build Coastguard Worker """Message with mbox- or MMDF-specific properties.""" 1637*cda5da8dSAndroid Build Coastguard Worker 1638*cda5da8dSAndroid Build Coastguard Worker _type_specific_attributes = ['_from'] 1639*cda5da8dSAndroid Build Coastguard Worker 1640*cda5da8dSAndroid Build Coastguard Worker def __init__(self, message=None): 1641*cda5da8dSAndroid Build Coastguard Worker """Initialize an mboxMMDFMessage instance.""" 1642*cda5da8dSAndroid Build Coastguard Worker self.set_from('MAILER-DAEMON', True) 1643*cda5da8dSAndroid Build Coastguard Worker if isinstance(message, email.message.Message): 1644*cda5da8dSAndroid Build Coastguard Worker unixfrom = message.get_unixfrom() 1645*cda5da8dSAndroid Build Coastguard Worker if unixfrom is not None and unixfrom.startswith('From '): 1646*cda5da8dSAndroid Build Coastguard Worker self.set_from(unixfrom[5:]) 1647*cda5da8dSAndroid Build Coastguard Worker Message.__init__(self, message) 1648*cda5da8dSAndroid Build Coastguard Worker 1649*cda5da8dSAndroid Build Coastguard Worker def get_from(self): 1650*cda5da8dSAndroid Build Coastguard Worker """Return contents of "From " line.""" 1651*cda5da8dSAndroid Build Coastguard Worker return self._from 1652*cda5da8dSAndroid Build Coastguard Worker 1653*cda5da8dSAndroid Build Coastguard Worker def set_from(self, from_, time_=None): 1654*cda5da8dSAndroid Build Coastguard Worker """Set "From " line, formatting and appending time_ if specified.""" 1655*cda5da8dSAndroid Build Coastguard Worker if time_ is not None: 1656*cda5da8dSAndroid Build Coastguard Worker if time_ is True: 1657*cda5da8dSAndroid Build Coastguard Worker time_ = time.gmtime() 1658*cda5da8dSAndroid Build Coastguard Worker from_ += ' ' + time.asctime(time_) 1659*cda5da8dSAndroid Build Coastguard Worker self._from = from_ 1660*cda5da8dSAndroid Build Coastguard Worker 1661*cda5da8dSAndroid Build Coastguard Worker def get_flags(self): 1662*cda5da8dSAndroid Build Coastguard Worker """Return as a string the flags that are set.""" 1663*cda5da8dSAndroid Build Coastguard Worker return self.get('Status', '') + self.get('X-Status', '') 1664*cda5da8dSAndroid Build Coastguard Worker 1665*cda5da8dSAndroid Build Coastguard Worker def set_flags(self, flags): 1666*cda5da8dSAndroid Build Coastguard Worker """Set the given flags and unset all others.""" 1667*cda5da8dSAndroid Build Coastguard Worker flags = set(flags) 1668*cda5da8dSAndroid Build Coastguard Worker status_flags, xstatus_flags = '', '' 1669*cda5da8dSAndroid Build Coastguard Worker for flag in ('R', 'O'): 1670*cda5da8dSAndroid Build Coastguard Worker if flag in flags: 1671*cda5da8dSAndroid Build Coastguard Worker status_flags += flag 1672*cda5da8dSAndroid Build Coastguard Worker flags.remove(flag) 1673*cda5da8dSAndroid Build Coastguard Worker for flag in ('D', 'F', 'A'): 1674*cda5da8dSAndroid Build Coastguard Worker if flag in flags: 1675*cda5da8dSAndroid Build Coastguard Worker xstatus_flags += flag 1676*cda5da8dSAndroid Build Coastguard Worker flags.remove(flag) 1677*cda5da8dSAndroid Build Coastguard Worker xstatus_flags += ''.join(sorted(flags)) 1678*cda5da8dSAndroid Build Coastguard Worker try: 1679*cda5da8dSAndroid Build Coastguard Worker self.replace_header('Status', status_flags) 1680*cda5da8dSAndroid Build Coastguard Worker except KeyError: 1681*cda5da8dSAndroid Build Coastguard Worker self.add_header('Status', status_flags) 1682*cda5da8dSAndroid Build Coastguard Worker try: 1683*cda5da8dSAndroid Build Coastguard Worker self.replace_header('X-Status', xstatus_flags) 1684*cda5da8dSAndroid Build Coastguard Worker except KeyError: 1685*cda5da8dSAndroid Build Coastguard Worker self.add_header('X-Status', xstatus_flags) 1686*cda5da8dSAndroid Build Coastguard Worker 1687*cda5da8dSAndroid Build Coastguard Worker def add_flag(self, flag): 1688*cda5da8dSAndroid Build Coastguard Worker """Set the given flag(s) without changing others.""" 1689*cda5da8dSAndroid Build Coastguard Worker self.set_flags(''.join(set(self.get_flags()) | set(flag))) 1690*cda5da8dSAndroid Build Coastguard Worker 1691*cda5da8dSAndroid Build Coastguard Worker def remove_flag(self, flag): 1692*cda5da8dSAndroid Build Coastguard Worker """Unset the given string flag(s) without changing others.""" 1693*cda5da8dSAndroid Build Coastguard Worker if 'Status' in self or 'X-Status' in self: 1694*cda5da8dSAndroid Build Coastguard Worker self.set_flags(''.join(set(self.get_flags()) - set(flag))) 1695*cda5da8dSAndroid Build Coastguard Worker 1696*cda5da8dSAndroid Build Coastguard Worker def _explain_to(self, message): 1697*cda5da8dSAndroid Build Coastguard Worker """Copy mbox- or MMDF-specific state to message insofar as possible.""" 1698*cda5da8dSAndroid Build Coastguard Worker if isinstance(message, MaildirMessage): 1699*cda5da8dSAndroid Build Coastguard Worker flags = set(self.get_flags()) 1700*cda5da8dSAndroid Build Coastguard Worker if 'O' in flags: 1701*cda5da8dSAndroid Build Coastguard Worker message.set_subdir('cur') 1702*cda5da8dSAndroid Build Coastguard Worker if 'F' in flags: 1703*cda5da8dSAndroid Build Coastguard Worker message.add_flag('F') 1704*cda5da8dSAndroid Build Coastguard Worker if 'A' in flags: 1705*cda5da8dSAndroid Build Coastguard Worker message.add_flag('R') 1706*cda5da8dSAndroid Build Coastguard Worker if 'R' in flags: 1707*cda5da8dSAndroid Build Coastguard Worker message.add_flag('S') 1708*cda5da8dSAndroid Build Coastguard Worker if 'D' in flags: 1709*cda5da8dSAndroid Build Coastguard Worker message.add_flag('T') 1710*cda5da8dSAndroid Build Coastguard Worker del message['status'] 1711*cda5da8dSAndroid Build Coastguard Worker del message['x-status'] 1712*cda5da8dSAndroid Build Coastguard Worker maybe_date = ' '.join(self.get_from().split()[-5:]) 1713*cda5da8dSAndroid Build Coastguard Worker try: 1714*cda5da8dSAndroid Build Coastguard Worker message.set_date(calendar.timegm(time.strptime(maybe_date, 1715*cda5da8dSAndroid Build Coastguard Worker '%a %b %d %H:%M:%S %Y'))) 1716*cda5da8dSAndroid Build Coastguard Worker except (ValueError, OverflowError): 1717*cda5da8dSAndroid Build Coastguard Worker pass 1718*cda5da8dSAndroid Build Coastguard Worker elif isinstance(message, _mboxMMDFMessage): 1719*cda5da8dSAndroid Build Coastguard Worker message.set_flags(self.get_flags()) 1720*cda5da8dSAndroid Build Coastguard Worker message.set_from(self.get_from()) 1721*cda5da8dSAndroid Build Coastguard Worker elif isinstance(message, MHMessage): 1722*cda5da8dSAndroid Build Coastguard Worker flags = set(self.get_flags()) 1723*cda5da8dSAndroid Build Coastguard Worker if 'R' not in flags: 1724*cda5da8dSAndroid Build Coastguard Worker message.add_sequence('unseen') 1725*cda5da8dSAndroid Build Coastguard Worker if 'A' in flags: 1726*cda5da8dSAndroid Build Coastguard Worker message.add_sequence('replied') 1727*cda5da8dSAndroid Build Coastguard Worker if 'F' in flags: 1728*cda5da8dSAndroid Build Coastguard Worker message.add_sequence('flagged') 1729*cda5da8dSAndroid Build Coastguard Worker del message['status'] 1730*cda5da8dSAndroid Build Coastguard Worker del message['x-status'] 1731*cda5da8dSAndroid Build Coastguard Worker elif isinstance(message, BabylMessage): 1732*cda5da8dSAndroid Build Coastguard Worker flags = set(self.get_flags()) 1733*cda5da8dSAndroid Build Coastguard Worker if 'R' not in flags: 1734*cda5da8dSAndroid Build Coastguard Worker message.add_label('unseen') 1735*cda5da8dSAndroid Build Coastguard Worker if 'D' in flags: 1736*cda5da8dSAndroid Build Coastguard Worker message.add_label('deleted') 1737*cda5da8dSAndroid Build Coastguard Worker if 'A' in flags: 1738*cda5da8dSAndroid Build Coastguard Worker message.add_label('answered') 1739*cda5da8dSAndroid Build Coastguard Worker del message['status'] 1740*cda5da8dSAndroid Build Coastguard Worker del message['x-status'] 1741*cda5da8dSAndroid Build Coastguard Worker elif isinstance(message, Message): 1742*cda5da8dSAndroid Build Coastguard Worker pass 1743*cda5da8dSAndroid Build Coastguard Worker else: 1744*cda5da8dSAndroid Build Coastguard Worker raise TypeError('Cannot convert to specified type: %s' % 1745*cda5da8dSAndroid Build Coastguard Worker type(message)) 1746*cda5da8dSAndroid Build Coastguard Worker 1747*cda5da8dSAndroid Build Coastguard Worker 1748*cda5da8dSAndroid Build Coastguard Workerclass mboxMessage(_mboxMMDFMessage): 1749*cda5da8dSAndroid Build Coastguard Worker """Message with mbox-specific properties.""" 1750*cda5da8dSAndroid Build Coastguard Worker 1751*cda5da8dSAndroid Build Coastguard Worker 1752*cda5da8dSAndroid Build Coastguard Workerclass MHMessage(Message): 1753*cda5da8dSAndroid Build Coastguard Worker """Message with MH-specific properties.""" 1754*cda5da8dSAndroid Build Coastguard Worker 1755*cda5da8dSAndroid Build Coastguard Worker _type_specific_attributes = ['_sequences'] 1756*cda5da8dSAndroid Build Coastguard Worker 1757*cda5da8dSAndroid Build Coastguard Worker def __init__(self, message=None): 1758*cda5da8dSAndroid Build Coastguard Worker """Initialize an MHMessage instance.""" 1759*cda5da8dSAndroid Build Coastguard Worker self._sequences = [] 1760*cda5da8dSAndroid Build Coastguard Worker Message.__init__(self, message) 1761*cda5da8dSAndroid Build Coastguard Worker 1762*cda5da8dSAndroid Build Coastguard Worker def get_sequences(self): 1763*cda5da8dSAndroid Build Coastguard Worker """Return a list of sequences that include the message.""" 1764*cda5da8dSAndroid Build Coastguard Worker return self._sequences[:] 1765*cda5da8dSAndroid Build Coastguard Worker 1766*cda5da8dSAndroid Build Coastguard Worker def set_sequences(self, sequences): 1767*cda5da8dSAndroid Build Coastguard Worker """Set the list of sequences that include the message.""" 1768*cda5da8dSAndroid Build Coastguard Worker self._sequences = list(sequences) 1769*cda5da8dSAndroid Build Coastguard Worker 1770*cda5da8dSAndroid Build Coastguard Worker def add_sequence(self, sequence): 1771*cda5da8dSAndroid Build Coastguard Worker """Add sequence to list of sequences including the message.""" 1772*cda5da8dSAndroid Build Coastguard Worker if isinstance(sequence, str): 1773*cda5da8dSAndroid Build Coastguard Worker if not sequence in self._sequences: 1774*cda5da8dSAndroid Build Coastguard Worker self._sequences.append(sequence) 1775*cda5da8dSAndroid Build Coastguard Worker else: 1776*cda5da8dSAndroid Build Coastguard Worker raise TypeError('sequence type must be str: %s' % type(sequence)) 1777*cda5da8dSAndroid Build Coastguard Worker 1778*cda5da8dSAndroid Build Coastguard Worker def remove_sequence(self, sequence): 1779*cda5da8dSAndroid Build Coastguard Worker """Remove sequence from the list of sequences including the message.""" 1780*cda5da8dSAndroid Build Coastguard Worker try: 1781*cda5da8dSAndroid Build Coastguard Worker self._sequences.remove(sequence) 1782*cda5da8dSAndroid Build Coastguard Worker except ValueError: 1783*cda5da8dSAndroid Build Coastguard Worker pass 1784*cda5da8dSAndroid Build Coastguard Worker 1785*cda5da8dSAndroid Build Coastguard Worker def _explain_to(self, message): 1786*cda5da8dSAndroid Build Coastguard Worker """Copy MH-specific state to message insofar as possible.""" 1787*cda5da8dSAndroid Build Coastguard Worker if isinstance(message, MaildirMessage): 1788*cda5da8dSAndroid Build Coastguard Worker sequences = set(self.get_sequences()) 1789*cda5da8dSAndroid Build Coastguard Worker if 'unseen' in sequences: 1790*cda5da8dSAndroid Build Coastguard Worker message.set_subdir('cur') 1791*cda5da8dSAndroid Build Coastguard Worker else: 1792*cda5da8dSAndroid Build Coastguard Worker message.set_subdir('cur') 1793*cda5da8dSAndroid Build Coastguard Worker message.add_flag('S') 1794*cda5da8dSAndroid Build Coastguard Worker if 'flagged' in sequences: 1795*cda5da8dSAndroid Build Coastguard Worker message.add_flag('F') 1796*cda5da8dSAndroid Build Coastguard Worker if 'replied' in sequences: 1797*cda5da8dSAndroid Build Coastguard Worker message.add_flag('R') 1798*cda5da8dSAndroid Build Coastguard Worker elif isinstance(message, _mboxMMDFMessage): 1799*cda5da8dSAndroid Build Coastguard Worker sequences = set(self.get_sequences()) 1800*cda5da8dSAndroid Build Coastguard Worker if 'unseen' not in sequences: 1801*cda5da8dSAndroid Build Coastguard Worker message.add_flag('RO') 1802*cda5da8dSAndroid Build Coastguard Worker else: 1803*cda5da8dSAndroid Build Coastguard Worker message.add_flag('O') 1804*cda5da8dSAndroid Build Coastguard Worker if 'flagged' in sequences: 1805*cda5da8dSAndroid Build Coastguard Worker message.add_flag('F') 1806*cda5da8dSAndroid Build Coastguard Worker if 'replied' in sequences: 1807*cda5da8dSAndroid Build Coastguard Worker message.add_flag('A') 1808*cda5da8dSAndroid Build Coastguard Worker elif isinstance(message, MHMessage): 1809*cda5da8dSAndroid Build Coastguard Worker for sequence in self.get_sequences(): 1810*cda5da8dSAndroid Build Coastguard Worker message.add_sequence(sequence) 1811*cda5da8dSAndroid Build Coastguard Worker elif isinstance(message, BabylMessage): 1812*cda5da8dSAndroid Build Coastguard Worker sequences = set(self.get_sequences()) 1813*cda5da8dSAndroid Build Coastguard Worker if 'unseen' in sequences: 1814*cda5da8dSAndroid Build Coastguard Worker message.add_label('unseen') 1815*cda5da8dSAndroid Build Coastguard Worker if 'replied' in sequences: 1816*cda5da8dSAndroid Build Coastguard Worker message.add_label('answered') 1817*cda5da8dSAndroid Build Coastguard Worker elif isinstance(message, Message): 1818*cda5da8dSAndroid Build Coastguard Worker pass 1819*cda5da8dSAndroid Build Coastguard Worker else: 1820*cda5da8dSAndroid Build Coastguard Worker raise TypeError('Cannot convert to specified type: %s' % 1821*cda5da8dSAndroid Build Coastguard Worker type(message)) 1822*cda5da8dSAndroid Build Coastguard Worker 1823*cda5da8dSAndroid Build Coastguard Worker 1824*cda5da8dSAndroid Build Coastguard Workerclass BabylMessage(Message): 1825*cda5da8dSAndroid Build Coastguard Worker """Message with Babyl-specific properties.""" 1826*cda5da8dSAndroid Build Coastguard Worker 1827*cda5da8dSAndroid Build Coastguard Worker _type_specific_attributes = ['_labels', '_visible'] 1828*cda5da8dSAndroid Build Coastguard Worker 1829*cda5da8dSAndroid Build Coastguard Worker def __init__(self, message=None): 1830*cda5da8dSAndroid Build Coastguard Worker """Initialize a BabylMessage instance.""" 1831*cda5da8dSAndroid Build Coastguard Worker self._labels = [] 1832*cda5da8dSAndroid Build Coastguard Worker self._visible = Message() 1833*cda5da8dSAndroid Build Coastguard Worker Message.__init__(self, message) 1834*cda5da8dSAndroid Build Coastguard Worker 1835*cda5da8dSAndroid Build Coastguard Worker def get_labels(self): 1836*cda5da8dSAndroid Build Coastguard Worker """Return a list of labels on the message.""" 1837*cda5da8dSAndroid Build Coastguard Worker return self._labels[:] 1838*cda5da8dSAndroid Build Coastguard Worker 1839*cda5da8dSAndroid Build Coastguard Worker def set_labels(self, labels): 1840*cda5da8dSAndroid Build Coastguard Worker """Set the list of labels on the message.""" 1841*cda5da8dSAndroid Build Coastguard Worker self._labels = list(labels) 1842*cda5da8dSAndroid Build Coastguard Worker 1843*cda5da8dSAndroid Build Coastguard Worker def add_label(self, label): 1844*cda5da8dSAndroid Build Coastguard Worker """Add label to list of labels on the message.""" 1845*cda5da8dSAndroid Build Coastguard Worker if isinstance(label, str): 1846*cda5da8dSAndroid Build Coastguard Worker if label not in self._labels: 1847*cda5da8dSAndroid Build Coastguard Worker self._labels.append(label) 1848*cda5da8dSAndroid Build Coastguard Worker else: 1849*cda5da8dSAndroid Build Coastguard Worker raise TypeError('label must be a string: %s' % type(label)) 1850*cda5da8dSAndroid Build Coastguard Worker 1851*cda5da8dSAndroid Build Coastguard Worker def remove_label(self, label): 1852*cda5da8dSAndroid Build Coastguard Worker """Remove label from the list of labels on the message.""" 1853*cda5da8dSAndroid Build Coastguard Worker try: 1854*cda5da8dSAndroid Build Coastguard Worker self._labels.remove(label) 1855*cda5da8dSAndroid Build Coastguard Worker except ValueError: 1856*cda5da8dSAndroid Build Coastguard Worker pass 1857*cda5da8dSAndroid Build Coastguard Worker 1858*cda5da8dSAndroid Build Coastguard Worker def get_visible(self): 1859*cda5da8dSAndroid Build Coastguard Worker """Return a Message representation of visible headers.""" 1860*cda5da8dSAndroid Build Coastguard Worker return Message(self._visible) 1861*cda5da8dSAndroid Build Coastguard Worker 1862*cda5da8dSAndroid Build Coastguard Worker def set_visible(self, visible): 1863*cda5da8dSAndroid Build Coastguard Worker """Set the Message representation of visible headers.""" 1864*cda5da8dSAndroid Build Coastguard Worker self._visible = Message(visible) 1865*cda5da8dSAndroid Build Coastguard Worker 1866*cda5da8dSAndroid Build Coastguard Worker def update_visible(self): 1867*cda5da8dSAndroid Build Coastguard Worker """Update and/or sensibly generate a set of visible headers.""" 1868*cda5da8dSAndroid Build Coastguard Worker for header in self._visible.keys(): 1869*cda5da8dSAndroid Build Coastguard Worker if header in self: 1870*cda5da8dSAndroid Build Coastguard Worker self._visible.replace_header(header, self[header]) 1871*cda5da8dSAndroid Build Coastguard Worker else: 1872*cda5da8dSAndroid Build Coastguard Worker del self._visible[header] 1873*cda5da8dSAndroid Build Coastguard Worker for header in ('Date', 'From', 'Reply-To', 'To', 'CC', 'Subject'): 1874*cda5da8dSAndroid Build Coastguard Worker if header in self and header not in self._visible: 1875*cda5da8dSAndroid Build Coastguard Worker self._visible[header] = self[header] 1876*cda5da8dSAndroid Build Coastguard Worker 1877*cda5da8dSAndroid Build Coastguard Worker def _explain_to(self, message): 1878*cda5da8dSAndroid Build Coastguard Worker """Copy Babyl-specific state to message insofar as possible.""" 1879*cda5da8dSAndroid Build Coastguard Worker if isinstance(message, MaildirMessage): 1880*cda5da8dSAndroid Build Coastguard Worker labels = set(self.get_labels()) 1881*cda5da8dSAndroid Build Coastguard Worker if 'unseen' in labels: 1882*cda5da8dSAndroid Build Coastguard Worker message.set_subdir('cur') 1883*cda5da8dSAndroid Build Coastguard Worker else: 1884*cda5da8dSAndroid Build Coastguard Worker message.set_subdir('cur') 1885*cda5da8dSAndroid Build Coastguard Worker message.add_flag('S') 1886*cda5da8dSAndroid Build Coastguard Worker if 'forwarded' in labels or 'resent' in labels: 1887*cda5da8dSAndroid Build Coastguard Worker message.add_flag('P') 1888*cda5da8dSAndroid Build Coastguard Worker if 'answered' in labels: 1889*cda5da8dSAndroid Build Coastguard Worker message.add_flag('R') 1890*cda5da8dSAndroid Build Coastguard Worker if 'deleted' in labels: 1891*cda5da8dSAndroid Build Coastguard Worker message.add_flag('T') 1892*cda5da8dSAndroid Build Coastguard Worker elif isinstance(message, _mboxMMDFMessage): 1893*cda5da8dSAndroid Build Coastguard Worker labels = set(self.get_labels()) 1894*cda5da8dSAndroid Build Coastguard Worker if 'unseen' not in labels: 1895*cda5da8dSAndroid Build Coastguard Worker message.add_flag('RO') 1896*cda5da8dSAndroid Build Coastguard Worker else: 1897*cda5da8dSAndroid Build Coastguard Worker message.add_flag('O') 1898*cda5da8dSAndroid Build Coastguard Worker if 'deleted' in labels: 1899*cda5da8dSAndroid Build Coastguard Worker message.add_flag('D') 1900*cda5da8dSAndroid Build Coastguard Worker if 'answered' in labels: 1901*cda5da8dSAndroid Build Coastguard Worker message.add_flag('A') 1902*cda5da8dSAndroid Build Coastguard Worker elif isinstance(message, MHMessage): 1903*cda5da8dSAndroid Build Coastguard Worker labels = set(self.get_labels()) 1904*cda5da8dSAndroid Build Coastguard Worker if 'unseen' in labels: 1905*cda5da8dSAndroid Build Coastguard Worker message.add_sequence('unseen') 1906*cda5da8dSAndroid Build Coastguard Worker if 'answered' in labels: 1907*cda5da8dSAndroid Build Coastguard Worker message.add_sequence('replied') 1908*cda5da8dSAndroid Build Coastguard Worker elif isinstance(message, BabylMessage): 1909*cda5da8dSAndroid Build Coastguard Worker message.set_visible(self.get_visible()) 1910*cda5da8dSAndroid Build Coastguard Worker for label in self.get_labels(): 1911*cda5da8dSAndroid Build Coastguard Worker message.add_label(label) 1912*cda5da8dSAndroid Build Coastguard Worker elif isinstance(message, Message): 1913*cda5da8dSAndroid Build Coastguard Worker pass 1914*cda5da8dSAndroid Build Coastguard Worker else: 1915*cda5da8dSAndroid Build Coastguard Worker raise TypeError('Cannot convert to specified type: %s' % 1916*cda5da8dSAndroid Build Coastguard Worker type(message)) 1917*cda5da8dSAndroid Build Coastguard Worker 1918*cda5da8dSAndroid Build Coastguard Worker 1919*cda5da8dSAndroid Build Coastguard Workerclass MMDFMessage(_mboxMMDFMessage): 1920*cda5da8dSAndroid Build Coastguard Worker """Message with MMDF-specific properties.""" 1921*cda5da8dSAndroid Build Coastguard Worker 1922*cda5da8dSAndroid Build Coastguard Worker 1923*cda5da8dSAndroid Build Coastguard Workerclass _ProxyFile: 1924*cda5da8dSAndroid Build Coastguard Worker """A read-only wrapper of a file.""" 1925*cda5da8dSAndroid Build Coastguard Worker 1926*cda5da8dSAndroid Build Coastguard Worker def __init__(self, f, pos=None): 1927*cda5da8dSAndroid Build Coastguard Worker """Initialize a _ProxyFile.""" 1928*cda5da8dSAndroid Build Coastguard Worker self._file = f 1929*cda5da8dSAndroid Build Coastguard Worker if pos is None: 1930*cda5da8dSAndroid Build Coastguard Worker self._pos = f.tell() 1931*cda5da8dSAndroid Build Coastguard Worker else: 1932*cda5da8dSAndroid Build Coastguard Worker self._pos = pos 1933*cda5da8dSAndroid Build Coastguard Worker 1934*cda5da8dSAndroid Build Coastguard Worker def read(self, size=None): 1935*cda5da8dSAndroid Build Coastguard Worker """Read bytes.""" 1936*cda5da8dSAndroid Build Coastguard Worker return self._read(size, self._file.read) 1937*cda5da8dSAndroid Build Coastguard Worker 1938*cda5da8dSAndroid Build Coastguard Worker def read1(self, size=None): 1939*cda5da8dSAndroid Build Coastguard Worker """Read bytes.""" 1940*cda5da8dSAndroid Build Coastguard Worker return self._read(size, self._file.read1) 1941*cda5da8dSAndroid Build Coastguard Worker 1942*cda5da8dSAndroid Build Coastguard Worker def readline(self, size=None): 1943*cda5da8dSAndroid Build Coastguard Worker """Read a line.""" 1944*cda5da8dSAndroid Build Coastguard Worker return self._read(size, self._file.readline) 1945*cda5da8dSAndroid Build Coastguard Worker 1946*cda5da8dSAndroid Build Coastguard Worker def readlines(self, sizehint=None): 1947*cda5da8dSAndroid Build Coastguard Worker """Read multiple lines.""" 1948*cda5da8dSAndroid Build Coastguard Worker result = [] 1949*cda5da8dSAndroid Build Coastguard Worker for line in self: 1950*cda5da8dSAndroid Build Coastguard Worker result.append(line) 1951*cda5da8dSAndroid Build Coastguard Worker if sizehint is not None: 1952*cda5da8dSAndroid Build Coastguard Worker sizehint -= len(line) 1953*cda5da8dSAndroid Build Coastguard Worker if sizehint <= 0: 1954*cda5da8dSAndroid Build Coastguard Worker break 1955*cda5da8dSAndroid Build Coastguard Worker return result 1956*cda5da8dSAndroid Build Coastguard Worker 1957*cda5da8dSAndroid Build Coastguard Worker def __iter__(self): 1958*cda5da8dSAndroid Build Coastguard Worker """Iterate over lines.""" 1959*cda5da8dSAndroid Build Coastguard Worker while True: 1960*cda5da8dSAndroid Build Coastguard Worker line = self.readline() 1961*cda5da8dSAndroid Build Coastguard Worker if not line: 1962*cda5da8dSAndroid Build Coastguard Worker return 1963*cda5da8dSAndroid Build Coastguard Worker yield line 1964*cda5da8dSAndroid Build Coastguard Worker 1965*cda5da8dSAndroid Build Coastguard Worker def tell(self): 1966*cda5da8dSAndroid Build Coastguard Worker """Return the position.""" 1967*cda5da8dSAndroid Build Coastguard Worker return self._pos 1968*cda5da8dSAndroid Build Coastguard Worker 1969*cda5da8dSAndroid Build Coastguard Worker def seek(self, offset, whence=0): 1970*cda5da8dSAndroid Build Coastguard Worker """Change position.""" 1971*cda5da8dSAndroid Build Coastguard Worker if whence == 1: 1972*cda5da8dSAndroid Build Coastguard Worker self._file.seek(self._pos) 1973*cda5da8dSAndroid Build Coastguard Worker self._file.seek(offset, whence) 1974*cda5da8dSAndroid Build Coastguard Worker self._pos = self._file.tell() 1975*cda5da8dSAndroid Build Coastguard Worker 1976*cda5da8dSAndroid Build Coastguard Worker def close(self): 1977*cda5da8dSAndroid Build Coastguard Worker """Close the file.""" 1978*cda5da8dSAndroid Build Coastguard Worker if hasattr(self, '_file'): 1979*cda5da8dSAndroid Build Coastguard Worker try: 1980*cda5da8dSAndroid Build Coastguard Worker if hasattr(self._file, 'close'): 1981*cda5da8dSAndroid Build Coastguard Worker self._file.close() 1982*cda5da8dSAndroid Build Coastguard Worker finally: 1983*cda5da8dSAndroid Build Coastguard Worker del self._file 1984*cda5da8dSAndroid Build Coastguard Worker 1985*cda5da8dSAndroid Build Coastguard Worker def _read(self, size, read_method): 1986*cda5da8dSAndroid Build Coastguard Worker """Read size bytes using read_method.""" 1987*cda5da8dSAndroid Build Coastguard Worker if size is None: 1988*cda5da8dSAndroid Build Coastguard Worker size = -1 1989*cda5da8dSAndroid Build Coastguard Worker self._file.seek(self._pos) 1990*cda5da8dSAndroid Build Coastguard Worker result = read_method(size) 1991*cda5da8dSAndroid Build Coastguard Worker self._pos = self._file.tell() 1992*cda5da8dSAndroid Build Coastguard Worker return result 1993*cda5da8dSAndroid Build Coastguard Worker 1994*cda5da8dSAndroid Build Coastguard Worker def __enter__(self): 1995*cda5da8dSAndroid Build Coastguard Worker """Context management protocol support.""" 1996*cda5da8dSAndroid Build Coastguard Worker return self 1997*cda5da8dSAndroid Build Coastguard Worker 1998*cda5da8dSAndroid Build Coastguard Worker def __exit__(self, *exc): 1999*cda5da8dSAndroid Build Coastguard Worker self.close() 2000*cda5da8dSAndroid Build Coastguard Worker 2001*cda5da8dSAndroid Build Coastguard Worker def readable(self): 2002*cda5da8dSAndroid Build Coastguard Worker return self._file.readable() 2003*cda5da8dSAndroid Build Coastguard Worker 2004*cda5da8dSAndroid Build Coastguard Worker def writable(self): 2005*cda5da8dSAndroid Build Coastguard Worker return self._file.writable() 2006*cda5da8dSAndroid Build Coastguard Worker 2007*cda5da8dSAndroid Build Coastguard Worker def seekable(self): 2008*cda5da8dSAndroid Build Coastguard Worker return self._file.seekable() 2009*cda5da8dSAndroid Build Coastguard Worker 2010*cda5da8dSAndroid Build Coastguard Worker def flush(self): 2011*cda5da8dSAndroid Build Coastguard Worker return self._file.flush() 2012*cda5da8dSAndroid Build Coastguard Worker 2013*cda5da8dSAndroid Build Coastguard Worker @property 2014*cda5da8dSAndroid Build Coastguard Worker def closed(self): 2015*cda5da8dSAndroid Build Coastguard Worker if not hasattr(self, '_file'): 2016*cda5da8dSAndroid Build Coastguard Worker return True 2017*cda5da8dSAndroid Build Coastguard Worker if not hasattr(self._file, 'closed'): 2018*cda5da8dSAndroid Build Coastguard Worker return False 2019*cda5da8dSAndroid Build Coastguard Worker return self._file.closed 2020*cda5da8dSAndroid Build Coastguard Worker 2021*cda5da8dSAndroid Build Coastguard Worker __class_getitem__ = classmethod(GenericAlias) 2022*cda5da8dSAndroid Build Coastguard Worker 2023*cda5da8dSAndroid Build Coastguard Worker 2024*cda5da8dSAndroid Build Coastguard Workerclass _PartialFile(_ProxyFile): 2025*cda5da8dSAndroid Build Coastguard Worker """A read-only wrapper of part of a file.""" 2026*cda5da8dSAndroid Build Coastguard Worker 2027*cda5da8dSAndroid Build Coastguard Worker def __init__(self, f, start=None, stop=None): 2028*cda5da8dSAndroid Build Coastguard Worker """Initialize a _PartialFile.""" 2029*cda5da8dSAndroid Build Coastguard Worker _ProxyFile.__init__(self, f, start) 2030*cda5da8dSAndroid Build Coastguard Worker self._start = start 2031*cda5da8dSAndroid Build Coastguard Worker self._stop = stop 2032*cda5da8dSAndroid Build Coastguard Worker 2033*cda5da8dSAndroid Build Coastguard Worker def tell(self): 2034*cda5da8dSAndroid Build Coastguard Worker """Return the position with respect to start.""" 2035*cda5da8dSAndroid Build Coastguard Worker return _ProxyFile.tell(self) - self._start 2036*cda5da8dSAndroid Build Coastguard Worker 2037*cda5da8dSAndroid Build Coastguard Worker def seek(self, offset, whence=0): 2038*cda5da8dSAndroid Build Coastguard Worker """Change position, possibly with respect to start or stop.""" 2039*cda5da8dSAndroid Build Coastguard Worker if whence == 0: 2040*cda5da8dSAndroid Build Coastguard Worker self._pos = self._start 2041*cda5da8dSAndroid Build Coastguard Worker whence = 1 2042*cda5da8dSAndroid Build Coastguard Worker elif whence == 2: 2043*cda5da8dSAndroid Build Coastguard Worker self._pos = self._stop 2044*cda5da8dSAndroid Build Coastguard Worker whence = 1 2045*cda5da8dSAndroid Build Coastguard Worker _ProxyFile.seek(self, offset, whence) 2046*cda5da8dSAndroid Build Coastguard Worker 2047*cda5da8dSAndroid Build Coastguard Worker def _read(self, size, read_method): 2048*cda5da8dSAndroid Build Coastguard Worker """Read size bytes using read_method, honoring start and stop.""" 2049*cda5da8dSAndroid Build Coastguard Worker remaining = self._stop - self._pos 2050*cda5da8dSAndroid Build Coastguard Worker if remaining <= 0: 2051*cda5da8dSAndroid Build Coastguard Worker return b'' 2052*cda5da8dSAndroid Build Coastguard Worker if size is None or size < 0 or size > remaining: 2053*cda5da8dSAndroid Build Coastguard Worker size = remaining 2054*cda5da8dSAndroid Build Coastguard Worker return _ProxyFile._read(self, size, read_method) 2055*cda5da8dSAndroid Build Coastguard Worker 2056*cda5da8dSAndroid Build Coastguard Worker def close(self): 2057*cda5da8dSAndroid Build Coastguard Worker # do *not* close the underlying file object for partial files, 2058*cda5da8dSAndroid Build Coastguard Worker # since it's global to the mailbox object 2059*cda5da8dSAndroid Build Coastguard Worker if hasattr(self, '_file'): 2060*cda5da8dSAndroid Build Coastguard Worker del self._file 2061*cda5da8dSAndroid Build Coastguard Worker 2062*cda5da8dSAndroid Build Coastguard Worker 2063*cda5da8dSAndroid Build Coastguard Workerdef _lock_file(f, dotlock=True): 2064*cda5da8dSAndroid Build Coastguard Worker """Lock file f using lockf and dot locking.""" 2065*cda5da8dSAndroid Build Coastguard Worker dotlock_done = False 2066*cda5da8dSAndroid Build Coastguard Worker try: 2067*cda5da8dSAndroid Build Coastguard Worker if fcntl: 2068*cda5da8dSAndroid Build Coastguard Worker try: 2069*cda5da8dSAndroid Build Coastguard Worker fcntl.lockf(f, fcntl.LOCK_EX | fcntl.LOCK_NB) 2070*cda5da8dSAndroid Build Coastguard Worker except OSError as e: 2071*cda5da8dSAndroid Build Coastguard Worker if e.errno in (errno.EAGAIN, errno.EACCES, errno.EROFS): 2072*cda5da8dSAndroid Build Coastguard Worker raise ExternalClashError('lockf: lock unavailable: %s' % 2073*cda5da8dSAndroid Build Coastguard Worker f.name) 2074*cda5da8dSAndroid Build Coastguard Worker else: 2075*cda5da8dSAndroid Build Coastguard Worker raise 2076*cda5da8dSAndroid Build Coastguard Worker if dotlock: 2077*cda5da8dSAndroid Build Coastguard Worker try: 2078*cda5da8dSAndroid Build Coastguard Worker pre_lock = _create_temporary(f.name + '.lock') 2079*cda5da8dSAndroid Build Coastguard Worker pre_lock.close() 2080*cda5da8dSAndroid Build Coastguard Worker except OSError as e: 2081*cda5da8dSAndroid Build Coastguard Worker if e.errno in (errno.EACCES, errno.EROFS): 2082*cda5da8dSAndroid Build Coastguard Worker return # Without write access, just skip dotlocking. 2083*cda5da8dSAndroid Build Coastguard Worker else: 2084*cda5da8dSAndroid Build Coastguard Worker raise 2085*cda5da8dSAndroid Build Coastguard Worker try: 2086*cda5da8dSAndroid Build Coastguard Worker try: 2087*cda5da8dSAndroid Build Coastguard Worker os.link(pre_lock.name, f.name + '.lock') 2088*cda5da8dSAndroid Build Coastguard Worker dotlock_done = True 2089*cda5da8dSAndroid Build Coastguard Worker except (AttributeError, PermissionError): 2090*cda5da8dSAndroid Build Coastguard Worker os.rename(pre_lock.name, f.name + '.lock') 2091*cda5da8dSAndroid Build Coastguard Worker dotlock_done = True 2092*cda5da8dSAndroid Build Coastguard Worker else: 2093*cda5da8dSAndroid Build Coastguard Worker os.unlink(pre_lock.name) 2094*cda5da8dSAndroid Build Coastguard Worker except FileExistsError: 2095*cda5da8dSAndroid Build Coastguard Worker os.remove(pre_lock.name) 2096*cda5da8dSAndroid Build Coastguard Worker raise ExternalClashError('dot lock unavailable: %s' % 2097*cda5da8dSAndroid Build Coastguard Worker f.name) 2098*cda5da8dSAndroid Build Coastguard Worker except: 2099*cda5da8dSAndroid Build Coastguard Worker if fcntl: 2100*cda5da8dSAndroid Build Coastguard Worker fcntl.lockf(f, fcntl.LOCK_UN) 2101*cda5da8dSAndroid Build Coastguard Worker if dotlock_done: 2102*cda5da8dSAndroid Build Coastguard Worker os.remove(f.name + '.lock') 2103*cda5da8dSAndroid Build Coastguard Worker raise 2104*cda5da8dSAndroid Build Coastguard Worker 2105*cda5da8dSAndroid Build Coastguard Workerdef _unlock_file(f): 2106*cda5da8dSAndroid Build Coastguard Worker """Unlock file f using lockf and dot locking.""" 2107*cda5da8dSAndroid Build Coastguard Worker if fcntl: 2108*cda5da8dSAndroid Build Coastguard Worker fcntl.lockf(f, fcntl.LOCK_UN) 2109*cda5da8dSAndroid Build Coastguard Worker if os.path.exists(f.name + '.lock'): 2110*cda5da8dSAndroid Build Coastguard Worker os.remove(f.name + '.lock') 2111*cda5da8dSAndroid Build Coastguard Worker 2112*cda5da8dSAndroid Build Coastguard Workerdef _create_carefully(path): 2113*cda5da8dSAndroid Build Coastguard Worker """Create a file if it doesn't exist and open for reading and writing.""" 2114*cda5da8dSAndroid Build Coastguard Worker fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_RDWR, 0o666) 2115*cda5da8dSAndroid Build Coastguard Worker try: 2116*cda5da8dSAndroid Build Coastguard Worker return open(path, 'rb+') 2117*cda5da8dSAndroid Build Coastguard Worker finally: 2118*cda5da8dSAndroid Build Coastguard Worker os.close(fd) 2119*cda5da8dSAndroid Build Coastguard Worker 2120*cda5da8dSAndroid Build Coastguard Workerdef _create_temporary(path): 2121*cda5da8dSAndroid Build Coastguard Worker """Create a temp file based on path and open for reading and writing.""" 2122*cda5da8dSAndroid Build Coastguard Worker return _create_carefully('%s.%s.%s.%s' % (path, int(time.time()), 2123*cda5da8dSAndroid Build Coastguard Worker socket.gethostname(), 2124*cda5da8dSAndroid Build Coastguard Worker os.getpid())) 2125*cda5da8dSAndroid Build Coastguard Worker 2126*cda5da8dSAndroid Build Coastguard Workerdef _sync_flush(f): 2127*cda5da8dSAndroid Build Coastguard Worker """Ensure changes to file f are physically on disk.""" 2128*cda5da8dSAndroid Build Coastguard Worker f.flush() 2129*cda5da8dSAndroid Build Coastguard Worker if hasattr(os, 'fsync'): 2130*cda5da8dSAndroid Build Coastguard Worker os.fsync(f.fileno()) 2131*cda5da8dSAndroid Build Coastguard Worker 2132*cda5da8dSAndroid Build Coastguard Workerdef _sync_close(f): 2133*cda5da8dSAndroid Build Coastguard Worker """Close file f, ensuring all changes are physically on disk.""" 2134*cda5da8dSAndroid Build Coastguard Worker _sync_flush(f) 2135*cda5da8dSAndroid Build Coastguard Worker f.close() 2136*cda5da8dSAndroid Build Coastguard Worker 2137*cda5da8dSAndroid Build Coastguard Worker 2138*cda5da8dSAndroid Build Coastguard Workerclass Error(Exception): 2139*cda5da8dSAndroid Build Coastguard Worker """Raised for module-specific errors.""" 2140*cda5da8dSAndroid Build Coastguard Worker 2141*cda5da8dSAndroid Build Coastguard Workerclass NoSuchMailboxError(Error): 2142*cda5da8dSAndroid Build Coastguard Worker """The specified mailbox does not exist and won't be created.""" 2143*cda5da8dSAndroid Build Coastguard Worker 2144*cda5da8dSAndroid Build Coastguard Workerclass NotEmptyError(Error): 2145*cda5da8dSAndroid Build Coastguard Worker """The specified mailbox is not empty and deletion was requested.""" 2146*cda5da8dSAndroid Build Coastguard Worker 2147*cda5da8dSAndroid Build Coastguard Workerclass ExternalClashError(Error): 2148*cda5da8dSAndroid Build Coastguard Worker """Another process caused an action to fail.""" 2149*cda5da8dSAndroid Build Coastguard Worker 2150*cda5da8dSAndroid Build Coastguard Workerclass FormatError(Error): 2151*cda5da8dSAndroid Build Coastguard Worker """A file appears to have an invalid format.""" 2152