PageRenderTime 118ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 0ms

/lib-python/2.7/mailbox.py

https://bitbucket.org/quangquach/pypy
Python | 2178 lines | 2158 code | 9 blank | 11 comment | 6 complexity | 9c2f53696b99b52496be326edb98672e MD5 | raw file

Large files files are truncated, but you can click here to view the full file

  1. #! /usr/bin/env python
  2. """Read/write support for Maildir, mbox, MH, Babyl, and MMDF mailboxes."""
  3. # Notes for authors of new mailbox subclasses:
  4. #
  5. # Remember to fsync() changes to disk before closing a modified file
  6. # or returning from a flush() method. See functions _sync_flush() and
  7. # _sync_close().
  8. import sys
  9. import os
  10. import time
  11. import calendar
  12. import socket
  13. import errno
  14. import copy
  15. import email
  16. import email.message
  17. import email.generator
  18. import StringIO
  19. try:
  20. if sys.platform == 'os2emx':
  21. # OS/2 EMX fcntl() not adequate
  22. raise ImportError
  23. import fcntl
  24. except ImportError:
  25. fcntl = None
  26. import warnings
  27. with warnings.catch_warnings():
  28. if sys.py3kwarning:
  29. warnings.filterwarnings("ignore", ".*rfc822 has been removed",
  30. DeprecationWarning)
  31. import rfc822
  32. __all__ = [ 'Mailbox', 'Maildir', 'mbox', 'MH', 'Babyl', 'MMDF',
  33. 'Message', 'MaildirMessage', 'mboxMessage', 'MHMessage',
  34. 'BabylMessage', 'MMDFMessage', 'UnixMailbox',
  35. 'PortableUnixMailbox', 'MmdfMailbox', 'MHMailbox', 'BabylMailbox' ]
  36. class Mailbox:
  37. """A group of messages in a particular place."""
  38. def __init__(self, path, factory=None, create=True):
  39. """Initialize a Mailbox instance."""
  40. self._path = os.path.abspath(os.path.expanduser(path))
  41. self._factory = factory
  42. def add(self, message):
  43. """Add message and return assigned key."""
  44. raise NotImplementedError('Method must be implemented by subclass')
  45. def remove(self, key):
  46. """Remove the keyed message; raise KeyError if it doesn't exist."""
  47. raise NotImplementedError('Method must be implemented by subclass')
  48. def __delitem__(self, key):
  49. self.remove(key)
  50. def discard(self, key):
  51. """If the keyed message exists, remove it."""
  52. try:
  53. self.remove(key)
  54. except KeyError:
  55. pass
  56. def __setitem__(self, key, message):
  57. """Replace the keyed message; raise KeyError if it doesn't exist."""
  58. raise NotImplementedError('Method must be implemented by subclass')
  59. def get(self, key, default=None):
  60. """Return the keyed message, or default if it doesn't exist."""
  61. try:
  62. return self.__getitem__(key)
  63. except KeyError:
  64. return default
  65. def __getitem__(self, key):
  66. """Return the keyed message; raise KeyError if it doesn't exist."""
  67. if not self._factory:
  68. return self.get_message(key)
  69. else:
  70. return self._factory(self.get_file(key))
  71. def get_message(self, key):
  72. """Return a Message representation or raise a KeyError."""
  73. raise NotImplementedError('Method must be implemented by subclass')
  74. def get_string(self, key):
  75. """Return a string representation or raise a KeyError."""
  76. raise NotImplementedError('Method must be implemented by subclass')
  77. def get_file(self, key):
  78. """Return a file-like representation or raise a KeyError."""
  79. raise NotImplementedError('Method must be implemented by subclass')
  80. def iterkeys(self):
  81. """Return an iterator over keys."""
  82. raise NotImplementedError('Method must be implemented by subclass')
  83. def keys(self):
  84. """Return a list of keys."""
  85. return list(self.iterkeys())
  86. def itervalues(self):
  87. """Return an iterator over all messages."""
  88. for key in self.iterkeys():
  89. try:
  90. value = self[key]
  91. except KeyError:
  92. continue
  93. yield value
  94. def __iter__(self):
  95. return self.itervalues()
  96. def values(self):
  97. """Return a list of messages. Memory intensive."""
  98. return list(self.itervalues())
  99. def iteritems(self):
  100. """Return an iterator over (key, message) tuples."""
  101. for key in self.iterkeys():
  102. try:
  103. value = self[key]
  104. except KeyError:
  105. continue
  106. yield (key, value)
  107. def items(self):
  108. """Return a list of (key, message) tuples. Memory intensive."""
  109. return list(self.iteritems())
  110. def has_key(self, key):
  111. """Return True if the keyed message exists, False otherwise."""
  112. raise NotImplementedError('Method must be implemented by subclass')
  113. def __contains__(self, key):
  114. return self.has_key(key)
  115. def __len__(self):
  116. """Return a count of messages in the mailbox."""
  117. raise NotImplementedError('Method must be implemented by subclass')
  118. def clear(self):
  119. """Delete all messages."""
  120. for key in self.iterkeys():
  121. self.discard(key)
  122. def pop(self, key, default=None):
  123. """Delete the keyed message and return it, or default."""
  124. try:
  125. result = self[key]
  126. except KeyError:
  127. return default
  128. self.discard(key)
  129. return result
  130. def popitem(self):
  131. """Delete an arbitrary (key, message) pair and return it."""
  132. for key in self.iterkeys():
  133. return (key, self.pop(key)) # This is only run once.
  134. else:
  135. raise KeyError('No messages in mailbox')
  136. def update(self, arg=None):
  137. """Change the messages that correspond to certain keys."""
  138. if hasattr(arg, 'iteritems'):
  139. source = arg.iteritems()
  140. elif hasattr(arg, 'items'):
  141. source = arg.items()
  142. else:
  143. source = arg
  144. bad_key = False
  145. for key, message in source:
  146. try:
  147. self[key] = message
  148. except KeyError:
  149. bad_key = True
  150. if bad_key:
  151. raise KeyError('No message with key(s)')
  152. def flush(self):
  153. """Write any pending changes to the disk."""
  154. raise NotImplementedError('Method must be implemented by subclass')
  155. def lock(self):
  156. """Lock the mailbox."""
  157. raise NotImplementedError('Method must be implemented by subclass')
  158. def unlock(self):
  159. """Unlock the mailbox if it is locked."""
  160. raise NotImplementedError('Method must be implemented by subclass')
  161. def close(self):
  162. """Flush and close the mailbox."""
  163. raise NotImplementedError('Method must be implemented by subclass')
  164. def _dump_message(self, message, target, mangle_from_=False):
  165. # Most files are opened in binary mode to allow predictable seeking.
  166. # To get native line endings on disk, the user-friendly \n line endings
  167. # used in strings and by email.Message are translated here.
  168. """Dump message contents to target file."""
  169. if isinstance(message, email.message.Message):
  170. buffer = StringIO.StringIO()
  171. gen = email.generator.Generator(buffer, mangle_from_, 0)
  172. gen.flatten(message)
  173. buffer.seek(0)
  174. target.write(buffer.read().replace('\n', os.linesep))
  175. elif isinstance(message, str):
  176. if mangle_from_:
  177. message = message.replace('\nFrom ', '\n>From ')
  178. message = message.replace('\n', os.linesep)
  179. target.write(message)
  180. elif hasattr(message, 'read'):
  181. while True:
  182. line = message.readline()
  183. if line == '':
  184. break
  185. if mangle_from_ and line.startswith('From '):
  186. line = '>From ' + line[5:]
  187. line = line.replace('\n', os.linesep)
  188. target.write(line)
  189. else:
  190. raise TypeError('Invalid message type: %s' % type(message))
  191. class Maildir(Mailbox):
  192. """A qmail-style Maildir mailbox."""
  193. colon = ':'
  194. def __init__(self, dirname, factory=rfc822.Message, create=True):
  195. """Initialize a Maildir instance."""
  196. Mailbox.__init__(self, dirname, factory, create)
  197. self._paths = {
  198. 'tmp': os.path.join(self._path, 'tmp'),
  199. 'new': os.path.join(self._path, 'new'),
  200. 'cur': os.path.join(self._path, 'cur'),
  201. }
  202. if not os.path.exists(self._path):
  203. if create:
  204. os.mkdir(self._path, 0700)
  205. for path in self._paths.values():
  206. os.mkdir(path, 0o700)
  207. else:
  208. raise NoSuchMailboxError(self._path)
  209. self._toc = {}
  210. self._toc_mtimes = {'cur': 0, 'new': 0}
  211. self._last_read = 0 # Records last time we read cur/new
  212. self._skewfactor = 0.1 # Adjust if os/fs clocks are skewing
  213. def add(self, message):
  214. """Add message and return assigned key."""
  215. tmp_file = self._create_tmp()
  216. try:
  217. self._dump_message(message, tmp_file)
  218. except BaseException:
  219. tmp_file.close()
  220. os.remove(tmp_file.name)
  221. raise
  222. _sync_close(tmp_file)
  223. if isinstance(message, MaildirMessage):
  224. subdir = message.get_subdir()
  225. suffix = self.colon + message.get_info()
  226. if suffix == self.colon:
  227. suffix = ''
  228. else:
  229. subdir = 'new'
  230. suffix = ''
  231. uniq = os.path.basename(tmp_file.name).split(self.colon)[0]
  232. dest = os.path.join(self._path, subdir, uniq + suffix)
  233. try:
  234. if hasattr(os, 'link'):
  235. os.link(tmp_file.name, dest)
  236. os.remove(tmp_file.name)
  237. else:
  238. os.rename(tmp_file.name, dest)
  239. except OSError, e:
  240. os.remove(tmp_file.name)
  241. if e.errno == errno.EEXIST:
  242. raise ExternalClashError('Name clash with existing message: %s'
  243. % dest)
  244. else:
  245. raise
  246. if isinstance(message, MaildirMessage):
  247. os.utime(dest, (os.path.getatime(dest), message.get_date()))
  248. return uniq
  249. def remove(self, key):
  250. """Remove the keyed message; raise KeyError if it doesn't exist."""
  251. os.remove(os.path.join(self._path, self._lookup(key)))
  252. def discard(self, key):
  253. """If the keyed message exists, remove it."""
  254. # This overrides an inapplicable implementation in the superclass.
  255. try:
  256. self.remove(key)
  257. except KeyError:
  258. pass
  259. except OSError, e:
  260. if e.errno != errno.ENOENT:
  261. raise
  262. def __setitem__(self, key, message):
  263. """Replace the keyed message; raise KeyError if it doesn't exist."""
  264. old_subpath = self._lookup(key)
  265. temp_key = self.add(message)
  266. temp_subpath = self._lookup(temp_key)
  267. if isinstance(message, MaildirMessage):
  268. # temp's subdir and suffix were specified by message.
  269. dominant_subpath = temp_subpath
  270. else:
  271. # temp's subdir and suffix were defaults from add().
  272. dominant_subpath = old_subpath
  273. subdir = os.path.dirname(dominant_subpath)
  274. if self.colon in dominant_subpath:
  275. suffix = self.colon + dominant_subpath.split(self.colon)[-1]
  276. else:
  277. suffix = ''
  278. self.discard(key)
  279. new_path = os.path.join(self._path, subdir, key + suffix)
  280. os.rename(os.path.join(self._path, temp_subpath), new_path)
  281. if isinstance(message, MaildirMessage):
  282. os.utime(new_path, (os.path.getatime(new_path),
  283. message.get_date()))
  284. def get_message(self, key):
  285. """Return a Message representation or raise a KeyError."""
  286. subpath = self._lookup(key)
  287. f = open(os.path.join(self._path, subpath), 'r')
  288. try:
  289. if self._factory:
  290. msg = self._factory(f)
  291. else:
  292. msg = MaildirMessage(f)
  293. finally:
  294. f.close()
  295. subdir, name = os.path.split(subpath)
  296. msg.set_subdir(subdir)
  297. if self.colon in name:
  298. msg.set_info(name.split(self.colon)[-1])
  299. msg.set_date(os.path.getmtime(os.path.join(self._path, subpath)))
  300. return msg
  301. def get_string(self, key):
  302. """Return a string representation or raise a KeyError."""
  303. f = open(os.path.join(self._path, self._lookup(key)), 'r')
  304. try:
  305. return f.read()
  306. finally:
  307. f.close()
  308. def get_file(self, key):
  309. """Return a file-like representation or raise a KeyError."""
  310. f = open(os.path.join(self._path, self._lookup(key)), 'rb')
  311. return _ProxyFile(f)
  312. def iterkeys(self):
  313. """Return an iterator over keys."""
  314. self._refresh()
  315. for key in self._toc:
  316. try:
  317. self._lookup(key)
  318. except KeyError:
  319. continue
  320. yield key
  321. def has_key(self, key):
  322. """Return True if the keyed message exists, False otherwise."""
  323. self._refresh()
  324. return key in self._toc
  325. def __len__(self):
  326. """Return a count of messages in the mailbox."""
  327. self._refresh()
  328. return len(self._toc)
  329. def flush(self):
  330. """Write any pending changes to disk."""
  331. # Maildir changes are always written immediately, so there's nothing
  332. # to do.
  333. pass
  334. def lock(self):
  335. """Lock the mailbox."""
  336. return
  337. def unlock(self):
  338. """Unlock the mailbox if it is locked."""
  339. return
  340. def close(self):
  341. """Flush and close the mailbox."""
  342. return
  343. def list_folders(self):
  344. """Return a list of folder names."""
  345. result = []
  346. for entry in os.listdir(self._path):
  347. if len(entry) > 1 and entry[0] == '.' and \
  348. os.path.isdir(os.path.join(self._path, entry)):
  349. result.append(entry[1:])
  350. return result
  351. def get_folder(self, folder):
  352. """Return a Maildir instance for the named folder."""
  353. return Maildir(os.path.join(self._path, '.' + folder),
  354. factory=self._factory,
  355. create=False)
  356. def add_folder(self, folder):
  357. """Create a folder and return a Maildir instance representing it."""
  358. path = os.path.join(self._path, '.' + folder)
  359. result = Maildir(path, factory=self._factory)
  360. maildirfolder_path = os.path.join(path, 'maildirfolder')
  361. if not os.path.exists(maildirfolder_path):
  362. os.close(os.open(maildirfolder_path, os.O_CREAT | os.O_WRONLY,
  363. 0666))
  364. return result
  365. def remove_folder(self, folder):
  366. """Delete the named folder, which must be empty."""
  367. path = os.path.join(self._path, '.' + folder)
  368. for entry in os.listdir(os.path.join(path, 'new')) + \
  369. os.listdir(os.path.join(path, 'cur')):
  370. if len(entry) < 1 or entry[0] != '.':
  371. raise NotEmptyError('Folder contains message(s): %s' % folder)
  372. for entry in os.listdir(path):
  373. if entry != 'new' and entry != 'cur' and entry != 'tmp' and \
  374. os.path.isdir(os.path.join(path, entry)):
  375. raise NotEmptyError("Folder contains subdirectory '%s': %s" %
  376. (folder, entry))
  377. for root, dirs, files in os.walk(path, topdown=False):
  378. for entry in files:
  379. os.remove(os.path.join(root, entry))
  380. for entry in dirs:
  381. os.rmdir(os.path.join(root, entry))
  382. os.rmdir(path)
  383. def clean(self):
  384. """Delete old files in "tmp"."""
  385. now = time.time()
  386. for entry in os.listdir(os.path.join(self._path, 'tmp')):
  387. path = os.path.join(self._path, 'tmp', entry)
  388. if now - os.path.getatime(path) > 129600: # 60 * 60 * 36
  389. os.remove(path)
  390. _count = 1 # This is used to generate unique file names.
  391. def _create_tmp(self):
  392. """Create a file in the tmp subdirectory and open and return it."""
  393. now = time.time()
  394. hostname = socket.gethostname()
  395. if '/' in hostname:
  396. hostname = hostname.replace('/', r'\057')
  397. if ':' in hostname:
  398. hostname = hostname.replace(':', r'\072')
  399. uniq = "%s.M%sP%sQ%s.%s" % (int(now), int(now % 1 * 1e6), os.getpid(),
  400. Maildir._count, hostname)
  401. path = os.path.join(self._path, 'tmp', uniq)
  402. try:
  403. os.stat(path)
  404. except OSError, e:
  405. if e.errno == errno.ENOENT:
  406. Maildir._count += 1
  407. try:
  408. return _create_carefully(path)
  409. except OSError, e:
  410. if e.errno != errno.EEXIST:
  411. raise
  412. else:
  413. raise
  414. # Fall through to here if stat succeeded or open raised EEXIST.
  415. raise ExternalClashError('Name clash prevented file creation: %s' %
  416. path)
  417. def _refresh(self):
  418. """Update table of contents mapping."""
  419. # If it has been less than two seconds since the last _refresh() call,
  420. # we have to unconditionally re-read the mailbox just in case it has
  421. # been modified, because os.path.mtime() has a 2 sec resolution in the
  422. # most common worst case (FAT) and a 1 sec resolution typically. This
  423. # results in a few unnecessary re-reads when _refresh() is called
  424. # multiple times in that interval, but once the clock ticks over, we
  425. # will only re-read as needed. Because the filesystem might be being
  426. # served by an independent system with its own clock, we record and
  427. # compare with the mtimes from the filesystem. Because the other
  428. # system's clock might be skewing relative to our clock, we add an
  429. # extra delta to our wait. The default is one tenth second, but is an
  430. # instance variable and so can be adjusted if dealing with a
  431. # particularly skewed or irregular system.
  432. if time.time() - self._last_read > 2 + self._skewfactor:
  433. refresh = False
  434. for subdir in self._toc_mtimes:
  435. mtime = os.path.getmtime(self._paths[subdir])
  436. if mtime > self._toc_mtimes[subdir]:
  437. refresh = True
  438. self._toc_mtimes[subdir] = mtime
  439. if not refresh:
  440. return
  441. # Refresh toc
  442. self._toc = {}
  443. for subdir in self._toc_mtimes:
  444. path = self._paths[subdir]
  445. for entry in os.listdir(path):
  446. p = os.path.join(path, entry)
  447. if os.path.isdir(p):
  448. continue
  449. uniq = entry.split(self.colon)[0]
  450. self._toc[uniq] = os.path.join(subdir, entry)
  451. self._last_read = time.time()
  452. def _lookup(self, key):
  453. """Use TOC to return subpath for given key, or raise a KeyError."""
  454. try:
  455. if os.path.exists(os.path.join(self._path, self._toc[key])):
  456. return self._toc[key]
  457. except KeyError:
  458. pass
  459. self._refresh()
  460. try:
  461. return self._toc[key]
  462. except KeyError:
  463. raise KeyError('No message with key: %s' % key)
  464. # This method is for backward compatibility only.
  465. def next(self):
  466. """Return the next message in a one-time iteration."""
  467. if not hasattr(self, '_onetime_keys'):
  468. self._onetime_keys = self.iterkeys()
  469. while True:
  470. try:
  471. return self[self._onetime_keys.next()]
  472. except StopIteration:
  473. return None
  474. except KeyError:
  475. continue
  476. class _singlefileMailbox(Mailbox):
  477. """A single-file mailbox."""
  478. def __init__(self, path, factory=None, create=True):
  479. """Initialize a single-file mailbox."""
  480. Mailbox.__init__(self, path, factory, create)
  481. try:
  482. f = open(self._path, 'rb+')
  483. except IOError, e:
  484. if e.errno == errno.ENOENT:
  485. if create:
  486. f = open(self._path, 'wb+')
  487. else:
  488. raise NoSuchMailboxError(self._path)
  489. elif e.errno in (errno.EACCES, errno.EROFS):
  490. f = open(self._path, 'rb')
  491. else:
  492. raise
  493. self._file = f
  494. self._toc = None
  495. self._next_key = 0
  496. self._pending = False # No changes require rewriting the file.
  497. self._locked = False
  498. self._file_length = None # Used to record mailbox size
  499. def add(self, message):
  500. """Add message and return assigned key."""
  501. self._lookup()
  502. self._toc[self._next_key] = self._append_message(message)
  503. self._next_key += 1
  504. self._pending = True
  505. return self._next_key - 1
  506. def remove(self, key):
  507. """Remove the keyed message; raise KeyError if it doesn't exist."""
  508. self._lookup(key)
  509. del self._toc[key]
  510. self._pending = True
  511. def __setitem__(self, key, message):
  512. """Replace the keyed message; raise KeyError if it doesn't exist."""
  513. self._lookup(key)
  514. self._toc[key] = self._append_message(message)
  515. self._pending = True
  516. def iterkeys(self):
  517. """Return an iterator over keys."""
  518. self._lookup()
  519. for key in self._toc.keys():
  520. yield key
  521. def has_key(self, key):
  522. """Return True if the keyed message exists, False otherwise."""
  523. self._lookup()
  524. return key in self._toc
  525. def __len__(self):
  526. """Return a count of messages in the mailbox."""
  527. self._lookup()
  528. return len(self._toc)
  529. def lock(self):
  530. """Lock the mailbox."""
  531. if not self._locked:
  532. _lock_file(self._file)
  533. self._locked = True
  534. def unlock(self):
  535. """Unlock the mailbox if it is locked."""
  536. if self._locked:
  537. _unlock_file(self._file)
  538. self._locked = False
  539. def flush(self):
  540. """Write any pending changes to disk."""
  541. if not self._pending:
  542. return
  543. # In order to be writing anything out at all, self._toc must
  544. # already have been generated (and presumably has been modified
  545. # by adding or deleting an item).
  546. assert self._toc is not None
  547. # Check length of self._file; if it's changed, some other process
  548. # has modified the mailbox since we scanned it.
  549. self._file.seek(0, 2)
  550. cur_len = self._file.tell()
  551. if cur_len != self._file_length:
  552. raise ExternalClashError('Size of mailbox file changed '
  553. '(expected %i, found %i)' %
  554. (self._file_length, cur_len))
  555. new_file = _create_temporary(self._path)
  556. try:
  557. new_toc = {}
  558. self._pre_mailbox_hook(new_file)
  559. for key in sorted(self._toc.keys()):
  560. start, stop = self._toc[key]
  561. self._file.seek(start)
  562. self._pre_message_hook(new_file)
  563. new_start = new_file.tell()
  564. while True:
  565. buffer = self._file.read(min(4096,
  566. stop - self._file.tell()))
  567. if buffer == '':
  568. break
  569. new_file.write(buffer)
  570. new_toc[key] = (new_start, new_file.tell())
  571. self._post_message_hook(new_file)
  572. except:
  573. new_file.close()
  574. os.remove(new_file.name)
  575. raise
  576. _sync_close(new_file)
  577. # self._file is about to get replaced, so no need to sync.
  578. self._file.close()
  579. try:
  580. os.rename(new_file.name, self._path)
  581. except OSError, e:
  582. if e.errno == errno.EEXIST or \
  583. (os.name == 'os2' and e.errno == errno.EACCES):
  584. os.remove(self._path)
  585. os.rename(new_file.name, self._path)
  586. else:
  587. raise
  588. self._file = open(self._path, 'rb+')
  589. self._toc = new_toc
  590. self._pending = False
  591. if self._locked:
  592. _lock_file(self._file, dotlock=False)
  593. def _pre_mailbox_hook(self, f):
  594. """Called before writing the mailbox to file f."""
  595. return
  596. def _pre_message_hook(self, f):
  597. """Called before writing each message to file f."""
  598. return
  599. def _post_message_hook(self, f):
  600. """Called after writing each message to file f."""
  601. return
  602. def close(self):
  603. """Flush and close the mailbox."""
  604. self.flush()
  605. if self._locked:
  606. self.unlock()
  607. self._file.close() # Sync has been done by self.flush() above.
  608. def _lookup(self, key=None):
  609. """Return (start, stop) or raise KeyError."""
  610. if self._toc is None:
  611. self._generate_toc()
  612. if key is not None:
  613. try:
  614. return self._toc[key]
  615. except KeyError:
  616. raise KeyError('No message with key: %s' % key)
  617. def _append_message(self, message):
  618. """Append message to mailbox and return (start, stop) offsets."""
  619. self._file.seek(0, 2)
  620. before = self._file.tell()
  621. try:
  622. self._pre_message_hook(self._file)
  623. offsets = self._install_message(message)
  624. self._post_message_hook(self._file)
  625. except BaseException:
  626. self._file.truncate(before)
  627. raise
  628. self._file.flush()
  629. self._file_length = self._file.tell() # Record current length of mailbox
  630. return offsets
  631. class _mboxMMDF(_singlefileMailbox):
  632. """An mbox or MMDF mailbox."""
  633. _mangle_from_ = True
  634. def get_message(self, key):
  635. """Return a Message representation or raise a KeyError."""
  636. start, stop = self._lookup(key)
  637. self._file.seek(start)
  638. from_line = self._file.readline().replace(os.linesep, '')
  639. string = self._file.read(stop - self._file.tell())
  640. msg = self._message_factory(string.replace(os.linesep, '\n'))
  641. msg.set_from(from_line[5:])
  642. return msg
  643. def get_string(self, key, from_=False):
  644. """Return a string representation or raise a KeyError."""
  645. start, stop = self._lookup(key)
  646. self._file.seek(start)
  647. if not from_:
  648. self._file.readline()
  649. string = self._file.read(stop - self._file.tell())
  650. return string.replace(os.linesep, '\n')
  651. def get_file(self, key, from_=False):
  652. """Return a file-like representation or raise a KeyError."""
  653. start, stop = self._lookup(key)
  654. self._file.seek(start)
  655. if not from_:
  656. self._file.readline()
  657. return _PartialFile(self._file, self._file.tell(), stop)
  658. def _install_message(self, message):
  659. """Format a message and blindly write to self._file."""
  660. from_line = None
  661. if isinstance(message, str) and message.startswith('From '):
  662. newline = message.find('\n')
  663. if newline != -1:
  664. from_line = message[:newline]
  665. message = message[newline + 1:]
  666. else:
  667. from_line = message
  668. message = ''
  669. elif isinstance(message, _mboxMMDFMessage):
  670. from_line = 'From ' + message.get_from()
  671. elif isinstance(message, email.message.Message):
  672. from_line = message.get_unixfrom() # May be None.
  673. if from_line is None:
  674. from_line = 'From MAILER-DAEMON %s' % time.asctime(time.gmtime())
  675. start = self._file.tell()
  676. self._file.write(from_line + os.linesep)
  677. self._dump_message(message, self._file, self._mangle_from_)
  678. stop = self._file.tell()
  679. return (start, stop)
  680. class mbox(_mboxMMDF):
  681. """A classic mbox mailbox."""
  682. _mangle_from_ = True
  683. def __init__(self, path, factory=None, create=True):
  684. """Initialize an mbox mailbox."""
  685. self._message_factory = mboxMessage
  686. _mboxMMDF.__init__(self, path, factory, create)
  687. def _pre_message_hook(self, f):
  688. """Called before writing each message to file f."""
  689. if f.tell() != 0:
  690. f.write(os.linesep)
  691. def _generate_toc(self):
  692. """Generate key-to-(start, stop) table of contents."""
  693. starts, stops = [], []
  694. self._file.seek(0)
  695. while True:
  696. line_pos = self._file.tell()
  697. line = self._file.readline()
  698. if line.startswith('From '):
  699. if len(stops) < len(starts):
  700. stops.append(line_pos - len(os.linesep))
  701. starts.append(line_pos)
  702. elif line == '':
  703. stops.append(line_pos)
  704. break
  705. self._toc = dict(enumerate(zip(starts, stops)))
  706. self._next_key = len(self._toc)
  707. self._file_length = self._file.tell()
  708. class MMDF(_mboxMMDF):
  709. """An MMDF mailbox."""
  710. def __init__(self, path, factory=None, create=True):
  711. """Initialize an MMDF mailbox."""
  712. self._message_factory = MMDFMessage
  713. _mboxMMDF.__init__(self, path, factory, create)
  714. def _pre_message_hook(self, f):
  715. """Called before writing each message to file f."""
  716. f.write('\001\001\001\001' + os.linesep)
  717. def _post_message_hook(self, f):
  718. """Called after writing each message to file f."""
  719. f.write(os.linesep + '\001\001\001\001' + os.linesep)
  720. def _generate_toc(self):
  721. """Generate key-to-(start, stop) table of contents."""
  722. starts, stops = [], []
  723. self._file.seek(0)
  724. next_pos = 0
  725. while True:
  726. line_pos = next_pos
  727. line = self._file.readline()
  728. next_pos = self._file.tell()
  729. if line.startswith('\001\001\001\001' + os.linesep):
  730. starts.append(next_pos)
  731. while True:
  732. line_pos = next_pos
  733. line = self._file.readline()
  734. next_pos = self._file.tell()
  735. if line == '\001\001\001\001' + os.linesep:
  736. stops.append(line_pos - len(os.linesep))
  737. break
  738. elif line == '':
  739. stops.append(line_pos)
  740. break
  741. elif line == '':
  742. break
  743. self._toc = dict(enumerate(zip(starts, stops)))
  744. self._next_key = len(self._toc)
  745. self._file.seek(0, 2)
  746. self._file_length = self._file.tell()
  747. class MH(Mailbox):
  748. """An MH mailbox."""
  749. def __init__(self, path, factory=None, create=True):
  750. """Initialize an MH instance."""
  751. Mailbox.__init__(self, path, factory, create)
  752. if not os.path.exists(self._path):
  753. if create:
  754. os.mkdir(self._path, 0700)
  755. os.close(os.open(os.path.join(self._path, '.mh_sequences'),
  756. os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0600))
  757. else:
  758. raise NoSuchMailboxError(self._path)
  759. self._locked = False
  760. def add(self, message):
  761. """Add message and return assigned key."""
  762. keys = self.keys()
  763. if len(keys) == 0:
  764. new_key = 1
  765. else:
  766. new_key = max(keys) + 1
  767. new_path = os.path.join(self._path, str(new_key))
  768. f = _create_carefully(new_path)
  769. closed = False
  770. try:
  771. if self._locked:
  772. _lock_file(f)
  773. try:
  774. try:
  775. self._dump_message(message, f)
  776. except BaseException:
  777. # Unlock and close so it can be deleted on Windows
  778. if self._locked:
  779. _unlock_file(f)
  780. _sync_close(f)
  781. closed = True
  782. os.remove(new_path)
  783. raise
  784. if isinstance(message, MHMessage):
  785. self._dump_sequences(message, new_key)
  786. finally:
  787. if self._locked:
  788. _unlock_file(f)
  789. finally:
  790. if not closed:
  791. _sync_close(f)
  792. return new_key
  793. def remove(self, key):
  794. """Remove the keyed message; raise KeyError if it doesn't exist."""
  795. path = os.path.join(self._path, str(key))
  796. try:
  797. f = open(path, 'rb+')
  798. except IOError, e:
  799. if e.errno == errno.ENOENT:
  800. raise KeyError('No message with key: %s' % key)
  801. else:
  802. raise
  803. else:
  804. f.close()
  805. os.remove(path)
  806. def __setitem__(self, key, message):
  807. """Replace the keyed message; raise KeyError if it doesn't exist."""
  808. path = os.path.join(self._path, str(key))
  809. try:
  810. f = open(path, 'rb+')
  811. except IOError, e:
  812. if e.errno == errno.ENOENT:
  813. raise KeyError('No message with key: %s' % key)
  814. else:
  815. raise
  816. try:
  817. if self._locked:
  818. _lock_file(f)
  819. try:
  820. os.close(os.open(path, os.O_WRONLY | os.O_TRUNC))
  821. self._dump_message(message, f)
  822. if isinstance(message, MHMessage):
  823. self._dump_sequences(message, key)
  824. finally:
  825. if self._locked:
  826. _unlock_file(f)
  827. finally:
  828. _sync_close(f)
  829. def get_message(self, key):
  830. """Return a Message representation or raise a KeyError."""
  831. try:
  832. if self._locked:
  833. f = open(os.path.join(self._path, str(key)), 'r+')
  834. else:
  835. f = open(os.path.join(self._path, str(key)), 'r')
  836. except IOError, e:
  837. if e.errno == errno.ENOENT:
  838. raise KeyError('No message with key: %s' % key)
  839. else:
  840. raise
  841. try:
  842. if self._locked:
  843. _lock_file(f)
  844. try:
  845. msg = MHMessage(f)
  846. finally:
  847. if self._locked:
  848. _unlock_file(f)
  849. finally:
  850. f.close()
  851. for name, key_list in self.get_sequences().iteritems():
  852. if key in key_list:
  853. msg.add_sequence(name)
  854. return msg
  855. def get_string(self, key):
  856. """Return a string representation or raise a KeyError."""
  857. try:
  858. if self._locked:
  859. f = open(os.path.join(self._path, str(key)), 'r+')
  860. else:
  861. f = open(os.path.join(self._path, str(key)), 'r')
  862. except IOError, e:
  863. if e.errno == errno.ENOENT:
  864. raise KeyError('No message with key: %s' % key)
  865. else:
  866. raise
  867. try:
  868. if self._locked:
  869. _lock_file(f)
  870. try:
  871. return f.read()
  872. finally:
  873. if self._locked:
  874. _unlock_file(f)
  875. finally:
  876. f.close()
  877. def get_file(self, key):
  878. """Return a file-like representation or raise a KeyError."""
  879. try:
  880. f = open(os.path.join(self._path, str(key)), 'rb')
  881. except IOError, e:
  882. if e.errno == errno.ENOENT:
  883. raise KeyError('No message with key: %s' % key)
  884. else:
  885. raise
  886. return _ProxyFile(f)
  887. def iterkeys(self):
  888. """Return an iterator over keys."""
  889. return iter(sorted(int(entry) for entry in os.listdir(self._path)
  890. if entry.isdigit()))
  891. def has_key(self, key):
  892. """Return True if the keyed message exists, False otherwise."""
  893. return os.path.exists(os.path.join(self._path, str(key)))
  894. def __len__(self):
  895. """Return a count of messages in the mailbox."""
  896. return len(list(self.iterkeys()))
  897. def lock(self):
  898. """Lock the mailbox."""
  899. if not self._locked:
  900. self._file = open(os.path.join(self._path, '.mh_sequences'), 'rb+')
  901. _lock_file(self._file)
  902. self._locked = True
  903. def unlock(self):
  904. """Unlock the mailbox if it is locked."""
  905. if self._locked:
  906. _unlock_file(self._file)
  907. _sync_close(self._file)
  908. del self._file
  909. self._locked = False
  910. def flush(self):
  911. """Write any pending changes to the disk."""
  912. return
  913. def close(self):
  914. """Flush and close the mailbox."""
  915. if self._locked:
  916. self.unlock()
  917. def list_folders(self):
  918. """Return a list of folder names."""
  919. result = []
  920. for entry in os.listdir(self._path):
  921. if os.path.isdir(os.path.join(self._path, entry)):
  922. result.append(entry)
  923. return result
  924. def get_folder(self, folder):
  925. """Return an MH instance for the named folder."""
  926. return MH(os.path.join(self._path, folder),
  927. factory=self._factory, create=False)
  928. def add_folder(self, folder):
  929. """Create a folder and return an MH instance representing it."""
  930. return MH(os.path.join(self._path, folder),
  931. factory=self._factory)
  932. def remove_folder(self, folder):
  933. """Delete the named folder, which must be empty."""
  934. path = os.path.join(self._path, folder)
  935. entries = os.listdir(path)
  936. if entries == ['.mh_sequences']:
  937. os.remove(os.path.join(path, '.mh_sequences'))
  938. elif entries == []:
  939. pass
  940. else:
  941. raise NotEmptyError('Folder not empty: %s' % self._path)
  942. os.rmdir(path)
  943. def get_sequences(self):
  944. """Return a name-to-key-list dictionary to define each sequence."""
  945. results = {}
  946. f = open(os.path.join(self._path, '.mh_sequences'), 'r')
  947. try:
  948. all_keys = set(self.keys())
  949. for line in f:
  950. try:
  951. name, contents = line.split(':')
  952. keys = set()
  953. for spec in contents.split():
  954. if spec.isdigit():
  955. keys.add(int(spec))
  956. else:
  957. start, stop = (int(x) for x in spec.split('-'))
  958. keys.update(range(start, stop + 1))
  959. results[name] = [key for key in sorted(keys) \
  960. if key in all_keys]
  961. if len(results[name]) == 0:
  962. del results[name]
  963. except ValueError:
  964. raise FormatError('Invalid sequence specification: %s' %
  965. line.rstrip())
  966. finally:
  967. f.close()
  968. return results
  969. def set_sequences(self, sequences):
  970. """Set sequences using the given name-to-key-list dictionary."""
  971. f = open(os.path.join(self._path, '.mh_sequences'), 'r+')
  972. try:
  973. os.close(os.open(f.name, os.O_WRONLY | os.O_TRUNC))
  974. for name, keys in sequences.iteritems():
  975. if len(keys) == 0:
  976. continue
  977. f.write('%s:' % name)
  978. prev = None
  979. completing = False
  980. for key in sorted(set(keys)):
  981. if key - 1 == prev:
  982. if not completing:
  983. completing = True
  984. f.write('-')
  985. elif completing:
  986. completing = False
  987. f.write('%s %s' % (prev, key))
  988. else:
  989. f.write(' %s' % key)
  990. prev = key
  991. if completing:
  992. f.write(str(prev) + '\n')
  993. else:
  994. f.write('\n')
  995. finally:
  996. _sync_close(f)
  997. def pack(self):
  998. """Re-name messages to eliminate numbering gaps. Invalidates keys."""
  999. sequences = self.get_sequences()
  1000. prev = 0
  1001. changes = []
  1002. for key in self.iterkeys():
  1003. if key - 1 != prev:
  1004. changes.append((key, prev + 1))
  1005. if hasattr(os, 'link'):
  1006. os.link(os.path.join(self._path, str(key)),
  1007. os.path.join(self._path, str(prev + 1)))
  1008. os.unlink(os.path.join(self._path, str(key)))
  1009. else:
  1010. os.rename(os.path.join(self._path, str(key)),
  1011. os.path.join(self._path, str(prev + 1)))
  1012. prev += 1
  1013. self._next_key = prev + 1
  1014. if len(changes) == 0:
  1015. return
  1016. for name, key_list in sequences.items():
  1017. for old, new in changes:
  1018. if old in key_list:
  1019. key_list[key_list.index(old)] = new
  1020. self.set_sequences(sequences)
  1021. def _dump_sequences(self, message, key):
  1022. """Inspect a new MHMessage and update sequences appropriately."""
  1023. pending_sequences = message.get_sequences()
  1024. all_sequences = self.get_sequences()
  1025. for name, key_list in all_sequences.iteritems():
  1026. if name in pending_sequences:
  1027. key_list.append(key)
  1028. elif key in key_list:
  1029. del key_list[key_list.index(key)]
  1030. for sequence in pending_sequences:
  1031. if sequence not in all_sequences:
  1032. all_sequences[sequence] = [key]
  1033. self.set_sequences(all_sequences)
  1034. class Babyl(_singlefileMailbox):
  1035. """An Rmail-style Babyl mailbox."""
  1036. _special_labels = frozenset(('unseen', 'deleted', 'filed', 'answered',
  1037. 'forwarded', 'edited', 'resent'))
  1038. def __init__(self, path, factory=None, create=True):
  1039. """Initialize a Babyl mailbox."""
  1040. _singlefileMailbox.__init__(self, path, factory, create)
  1041. self._labels = {}
  1042. def add(self, message):
  1043. """Add message and return assigned key."""
  1044. key = _singlefileMailbox.add(self, message)
  1045. if isinstance(message, BabylMessage):
  1046. self._labels[key] = message.get_labels()
  1047. return key
  1048. def remove(self, key):
  1049. """Remove the keyed message; raise KeyError if it doesn't exist."""
  1050. _singlefileMailbox.remove(self, key)
  1051. if key in self._labels:
  1052. del self._labels[key]
  1053. def __setitem__(self, key, message):
  1054. """Replace the keyed message; raise KeyError if it doesn't exist."""
  1055. _singlefileMailbox.__setitem__(self, key, message)
  1056. if isinstance(message, BabylMessage):
  1057. self._labels[key] = message.get_labels()
  1058. def get_message(self, key):
  1059. """Return a Message representation or raise a KeyError."""
  1060. start, stop = self._lookup(key)
  1061. self._file.seek(start)
  1062. self._file.readline() # Skip '1,' line specifying labels.
  1063. original_headers = StringIO.StringIO()
  1064. while True:
  1065. line = self._file.readline()
  1066. if line == '*** EOOH ***' + os.linesep or line == '':
  1067. break
  1068. original_headers.write(line.replace(os.linesep, '\n'))
  1069. visible_headers = StringIO.StringIO()
  1070. while True:
  1071. line = self._file.readline()
  1072. if line == os.linesep or line == '':
  1073. break
  1074. visible_headers.write(line.replace(os.linesep, '\n'))
  1075. body = self._file.read(stop - self._file.tell()).replace(os.linesep,
  1076. '\n')
  1077. msg = BabylMessage(original_headers.getvalue() + body)
  1078. msg.set_visible(visible_headers.getvalue())
  1079. if key in self._labels:
  1080. msg.set_labels(self._labels[key])
  1081. return msg
  1082. def get_string(self, key):
  1083. """Return a string representation or raise a KeyError."""
  1084. start, stop = self._lookup(key)
  1085. self._file.seek(start)
  1086. self._file.readline() # Skip '1,' line specifying labels.
  1087. original_headers = StringIO.StringIO()
  1088. while True:
  1089. line = self._file.readline()
  1090. if line == '*** EOOH ***' + os.linesep or line == '':
  1091. break
  1092. original_headers.write(line.replace(os.linesep, '\n'))
  1093. while True:
  1094. line = self._file.readline()
  1095. if line == os.linesep or line == '':
  1096. break
  1097. return original_headers.getvalue() + \
  1098. self._file.read(stop - self._file.tell()).replace(os.linesep,
  1099. '\n')
  1100. def get_file(self, key):
  1101. """Return a file-like representation or raise a KeyError."""
  1102. return StringIO.StringIO(self.get_string(key).replace('\n',
  1103. os.linesep))
  1104. def get_labels(self):
  1105. """Return a list of user-defined labels in the mailbox."""
  1106. self._lookup()
  1107. labels = set()
  1108. for label_list in self._labels.values():
  1109. labels.update(label_list)
  1110. labels.difference_update(self._special_labels)
  1111. return list(labels)
  1112. def _generate_toc(self):
  1113. """Generate key-to-(start, stop) table of contents."""
  1114. starts, stops = [], []
  1115. self._file.seek(0)
  1116. next_pos = 0
  1117. label_lists = []
  1118. while True:
  1119. line_pos = next_pos
  1120. line = self._file.readline()
  1121. next_pos = self._file.tell()
  1122. if line == '\037\014' + os.linesep:
  1123. if len(stops) < len(starts):
  1124. stops.append(line_pos - len(os.linesep))
  1125. starts.append(next_pos)
  1126. labels = [label.strip() for label
  1127. in self._file.readline()[1:].split(',')
  1128. if label.strip() != '']
  1129. label_lists.append(labels)
  1130. elif line == '\037' or line == '\037' + os.linesep:
  1131. if len(stops) < len(starts):
  1132. stops.append(line_pos - len(os.linesep))
  1133. elif line == '':
  1134. stops.append(line_pos - len(os.linesep))
  1135. break
  1136. self._toc = dict(enumerate(zip(starts, stops)))
  1137. self._labels = dict(enumerate(label_lists))
  1138. self._next_key = len(self._toc)
  1139. self._file.seek(0, 2)
  1140. self._file_length = self._file.tell()
  1141. def _pre_mailbox_hook(self, f):
  1142. """Called before writing the mailbox to file f."""
  1143. f.write('BABYL OPTIONS:%sVersion: 5%sLabels:%s%s\037' %
  1144. (os.linesep, os.linesep, ','.join(self.get_labels()),
  1145. os.linesep))
  1146. def _pre_message_hook(self, f):
  1147. """Called before writing each message to file f."""
  1148. f.write('\014' + os.linesep)
  1149. def _post_message_hook(self, f):
  1150. """Called after writing each message to file f."""
  1151. f.write(os.linesep + '\037')
  1152. def _install_message(self, message):
  1153. """Write message contents and return (start, stop)."""
  1154. start = self._file.tell()
  1155. if isinstance(message, BabylMessage):
  1156. special_labels = []
  1157. labels = []
  1158. for label in message.get_labels():
  1159. if label in self._special_labels:
  1160. special_labels.append(label)
  1161. else:
  1162. labels.append(label)
  1163. self._file.write('1')
  1164. for label in special_labels:
  1165. self._file.write(', ' + label)
  1166. self._file.write(',,')
  1167. for label in labels:
  1168. self._file.write(' ' + label + ',')
  1169. self._file.write(os.linesep)
  1170. else:
  1171. self._file.write('1,,' + os.linesep)
  1172. if isinstance(message, email.message.Message):
  1173. orig_buffer = StringIO.StringIO()
  1174. orig_generator = email.generator.Generator(orig_buffer, False, 0)
  1175. orig_generator.flatten(message)
  1176. orig_buffer.seek(0)
  1177. while True:
  1178. line = orig_buffer.readline()
  1179. self._file.write(line.replace('\n', os.linesep))
  1180. if line == '\n' or line == '':
  1181. break
  1182. self._file.write('*** EOOH ***' + os.linesep)
  1183. if isinstance(message, BabylMessage):
  1184. vis_buffer = StringIO.StringIO()
  1185. vis_generator = email.generator.Generator(vis_buffer, False, 0)
  1186. vis_generator.flatten(message.get_visible())
  1187. while True:
  1188. line = vis_buffer.readline()
  1189. self._file.write(line.replace('\n', os.linesep))
  1190. if line == '\n' or line == '':
  1191. break
  1192. else:
  1193. orig_buffer.seek(0)
  1194. while True:
  1195. line = orig_buffer.readline()
  1196. self._file.write(line.replace('\n', os.linesep))
  1197. if line == '\n' or line == '':
  1198. break
  1199. while True:
  1200. buffer = orig_buffer.read(4096) # Buffer size is arbitrary.
  1201. if buffer == '':
  1202. break
  1203. self._file.write(buffer.replace('\n', os.linesep))
  1204. elif isinstance(message, str):
  1205. body_start = message.find('\n\n') + 2
  1206. if body_start - 2 != -1:
  1207. self._file.write(message[:body_start].replace('\n',
  1208. os.linesep))
  1209. self._file.write('*** EOOH ***' + os.linesep)
  1210. self._file.write(message[:body_start].replace('\n',
  1211. os.linesep))
  1212. self._file.write(message[body_start:].replace('\n',
  1213. os.linesep))
  1214. else:
  1215. self._file.write('*** EOOH ***' + os.linesep + os.linesep)

Large files files are truncated, but you can click here to view the full file