summaryrefslogtreecommitdiffstats
path: root/Lib/mailbox.py
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/mailbox.py')
-rw-r--r--Lib/mailbox.py94
1 files changed, 69 insertions, 25 deletions
diff --git a/Lib/mailbox.py b/Lib/mailbox.py
index 8b18010..b74eeb5 100644
--- a/Lib/mailbox.py
+++ b/Lib/mailbox.py
@@ -243,14 +243,21 @@ class Maildir(Mailbox):
else:
raise NoSuchMailboxError(self._path)
self._toc = {}
+ self._last_read = None # Records last time we read cur/new
+ # NOTE: we manually invalidate _last_read each time we do any
+ # modifications ourselves, otherwise we might get tripped up by
+ # bogus mtime behaviour on some systems (see issue #6896).
def add(self, message):
"""Add message and return assigned key."""
tmp_file = self._create_tmp()
try:
self._dump_message(message, tmp_file)
- finally:
- _sync_close(tmp_file)
+ except BaseException:
+ tmp_file.close()
+ os.remove(tmp_file.name)
+ raise
+ _sync_close(tmp_file)
if isinstance(message, MaildirMessage):
subdir = message.get_subdir()
suffix = self.colon + message.get_info()
@@ -276,11 +283,15 @@ class Maildir(Mailbox):
raise
if isinstance(message, MaildirMessage):
os.utime(dest, (os.path.getatime(dest), message.get_date()))
+ # Invalidate cached toc
+ self._last_read = None
return uniq
def remove(self, key):
"""Remove the keyed message; raise KeyError if it doesn't exist."""
os.remove(os.path.join(self._path, self._lookup(key)))
+ # Invalidate cached toc (only on success)
+ self._last_read = None
def discard(self, key):
"""If the keyed message exists, remove it."""
@@ -315,6 +326,8 @@ class Maildir(Mailbox):
if isinstance(message, MaildirMessage):
os.utime(new_path, (os.path.getatime(new_path),
message.get_date()))
+ # Invalidate cached toc
+ self._last_read = None
def get_message(self, key):
"""Return a Message representation or raise a KeyError."""
@@ -369,7 +382,9 @@ class Maildir(Mailbox):
def flush(self):
"""Write any pending changes to disk."""
- return # Maildir changes are always written immediately.
+ # Maildir changes are always written immediately, so there's nothing
+ # to do except invalidate our cached toc.
+ self._last_read = None
def lock(self):
"""Lock the mailbox."""
@@ -467,16 +482,37 @@ class Maildir(Mailbox):
def _refresh(self):
"""Update table of contents mapping."""
+ if self._last_read is not None:
+ for subdir in ('new', 'cur'):
+ mtime = os.path.getmtime(os.path.join(self._path, subdir))
+ if mtime > self._last_read:
+ break
+ else:
+ return
+
+ # We record the current time - 1sec so that, if _refresh() is called
+ # again in the same second, we will always re-read the mailbox
+ # just in case it's been modified. (os.path.mtime() only has
+ # 1sec resolution.) This results in a few unnecessary re-reads
+ # when _refresh() is called multiple times in the same second,
+ # but once the clock ticks over, we will only re-read as needed.
+ now = time.time() - 1
+
self._toc = {}
- for subdir in ('new', 'cur'):
- subdir_path = os.path.join(self._path, subdir)
- for entry in os.listdir(subdir_path):
- p = os.path.join(subdir_path, entry)
+ def update_dir (subdir):
+ path = os.path.join(self._path, subdir)
+ for entry in os.listdir(path):
+ p = os.path.join(path, entry)
if os.path.isdir(p):
continue
uniq = entry.split(self.colon)[0]
self._toc[uniq] = os.path.join(subdir, entry)
+ update_dir('new')
+ update_dir('cur')
+
+ self._last_read = now
+
def _lookup(self, key):
"""Use TOC to return subpath for given key, or raise a KeyError."""
try:
@@ -518,7 +554,7 @@ class _singlefileMailbox(Mailbox):
f = open(self._path, 'wb+')
else:
raise NoSuchMailboxError(self._path)
- elif e.errno == errno.EACCES:
+ elif e.errno in (errno.EACCES, errno.EROFS):
f = open(self._path, 'rb')
else:
raise
@@ -667,9 +703,14 @@ class _singlefileMailbox(Mailbox):
def _append_message(self, message):
"""Append message to mailbox and return (start, stop) offsets."""
self._file.seek(0, 2)
- self._pre_message_hook(self._file)
- offsets = self._install_message(message)
- self._post_message_hook(self._file)
+ before = self._file.tell()
+ try:
+ self._pre_message_hook(self._file)
+ offsets = self._install_message(message)
+ self._post_message_hook(self._file)
+ except BaseException:
+ self._file.truncate(before)
+ raise
self._file.flush()
self._file_length = self._file.tell() # Record current length of mailbox
return offsets
@@ -835,18 +876,29 @@ class MH(Mailbox):
new_key = max(keys) + 1
new_path = os.path.join(self._path, str(new_key))
f = _create_carefully(new_path)
+ closed = False
try:
if self._locked:
_lock_file(f)
try:
- self._dump_message(message, f)
+ try:
+ self._dump_message(message, f)
+ except BaseException:
+ # Unlock and close so it can be deleted on Windows
+ if self._locked:
+ _unlock_file(f)
+ _sync_close(f)
+ closed = True
+ os.remove(new_path)
+ raise
if isinstance(message, MHMessage):
self._dump_sequences(message, new_key)
finally:
if self._locked:
_unlock_file(f)
finally:
- _sync_close(f)
+ if not closed:
+ _sync_close(f)
return new_key
def remove(self, key):
@@ -859,17 +911,9 @@ class MH(Mailbox):
raise KeyError('No message with key: %s' % key)
else:
raise
- try:
- if self._locked:
- _lock_file(f)
- try:
- f.close()
- os.remove(os.path.join(self._path, str(key)))
- finally:
- if self._locked:
- _unlock_file(f)
- finally:
+ else:
f.close()
+ os.remove(path)
def __setitem__(self, key, message):
"""Replace the keyed message; raise KeyError if it doesn't exist."""
@@ -1861,7 +1905,7 @@ def _lock_file(f, dotlock=True):
try:
fcntl.lockf(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
except IOError, e:
- if e.errno in (errno.EAGAIN, errno.EACCES):
+ if e.errno in (errno.EAGAIN, errno.EACCES, errno.EROFS):
raise ExternalClashError('lockf: lock unavailable: %s' %
f.name)
else:
@@ -1871,7 +1915,7 @@ def _lock_file(f, dotlock=True):
pre_lock = _create_temporary(f.name + '.lock')
pre_lock.close()
except IOError, e:
- if e.errno == errno.EACCES:
+ if e.errno in (errno.EACCES, errno.EROFS):
return # Without write access, just skip dotlocking.
else:
raise