]> code.delx.au - offlineimap/blob - offlineimap/folder/Maildir.py
only-write-once-to-cur-or-new.patch
[offlineimap] / offlineimap / folder / Maildir.py
1 # Maildir folder support
2 # Copyright (C) 2002 - 2007 John Goerzen
3 # <jgoerzen@complete.org>
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18
19 from Base import BaseFolder
20 from offlineimap import imaputil
21 from offlineimap.ui import UIBase
22 from threading import Lock
23 import os.path, os, re, time, socket, md5
24
25 foldermatchre = re.compile(',FMD5=([0-9a-f]{32})')
26 uidmatchre = re.compile(',U=(\d+)')
27 flagmatchre = re.compile(':.*2,([A-Z]+)')
28
29 timeseq = 0
30 lasttime = long(0)
31 timelock = Lock()
32
33 def gettimeseq():
34 global lasttime, timeseq, timelock
35 timelock.acquire()
36 try:
37 thistime = long(time.time())
38 if thistime == lasttime:
39 timeseq += 1
40 return (thistime, timeseq)
41 else:
42 lasttime = thistime
43 timeseq = 0
44 return (thistime, timeseq)
45 finally:
46 timelock.release()
47
48 class MaildirFolder(BaseFolder):
49 def __init__(self, root, name, sep, repository, accountname):
50 self.name = name
51 self.root = root
52 self.sep = sep
53 self.messagelist = None
54 self.repository = repository
55 self.accountname = accountname
56 BaseFolder.__init__(self)
57
58 def getaccountname(self):
59 return self.accountname
60
61 def getfullname(self):
62 return os.path.join(self.getroot(), self.getname())
63
64 def getuidvalidity(self):
65 """Maildirs have no notion of uidvalidity, so we just return a magic
66 token."""
67 return 42
68
69 def _scanfolder(self):
70 """Cache the message list. Maildir flags are:
71 R (replied)
72 S (seen)
73 T (trashed)
74 D (draft)
75 F (flagged)
76 and must occur in ASCII order."""
77 retval = {}
78 files = []
79 nouidcounter = -1 # Messages without UIDs get
80 # negative UID numbers.
81 for dirannex in ['new', 'cur']:
82 fulldirname = os.path.join(self.getfullname(), dirannex)
83 files.extend([os.path.join(fulldirname, filename) for
84 filename in os.listdir(fulldirname)])
85 for file in files:
86 messagename = os.path.basename(file)
87 foldermatch = foldermatchre.search(messagename)
88 if (not foldermatch) or \
89 md5.new(self.getvisiblename()).hexdigest() \
90 != foldermatch.group(1):
91 # If there is no folder MD5 specified, or if it mismatches,
92 # assume it is a foreign (new) message and generate a
93 # negative uid for it
94 uid = nouidcounter
95 nouidcounter -= 1
96 else: # It comes from our folder.
97 uidmatch = uidmatchre.search(messagename)
98 uid = None
99 if not uidmatch:
100 uid = nouidcounter
101 nouidcounter -= 1
102 else:
103 uid = long(uidmatch.group(1))
104 flagmatch = flagmatchre.search(messagename)
105 flags = []
106 if flagmatch:
107 flags = [x for x in flagmatch.group(1)]
108 flags.sort()
109 retval[uid] = {'uid': uid,
110 'flags': flags,
111 'filename': file}
112 return retval
113
114 def cachemessagelist(self):
115 self.messagelist = self._scanfolder()
116
117 def getmessagelist(self):
118 return self.messagelist
119
120 def getmessage(self, uid):
121 filename = self.messagelist[uid]['filename']
122 file = open(filename, 'rt')
123 retval = file.read()
124 file.close()
125 return retval.replace("\r\n", "\n")
126
127 def getmessagetime( self, uid ):
128 filename = self.messagelist[uid]['filename']
129 st = os.stat(filename)
130 return st.st_mtime
131
132 def savemessage(self, uid, content, flags, rtime):
133 # This function only ever saves to tmp/,
134 # but it calls savemessageflags() to actually save to cur/ or new/.
135 ui = UIBase.getglobalui()
136 ui.debug('maildir', 'savemessage: called to write with flags %s and content %s' % \
137 (repr(flags), repr(content)))
138 if uid < 0:
139 # We cannot assign a new uid.
140 return uid
141 if uid in self.messagelist:
142 # We already have it.
143 self.savemessageflags(uid, flags)
144 return uid
145
146 # Otherwise, save the message in tmp/ and then call savemessageflags()
147 # to give it a permanent home.
148 tmpdir = os.path.join(self.getfullname(), 'tmp')
149 messagename = None
150 attempts = 0
151 while 1:
152 if attempts > 15:
153 raise IOError, "Couldn't write to file %s" % messagename
154 timeval, timeseq = gettimeseq()
155 messagename = '%d_%d.%d.%s,U=%d,FMD5=%s' % \
156 (timeval,
157 timeseq,
158 os.getpid(),
159 socket.gethostname(),
160 uid,
161 md5.new(self.getvisiblename()).hexdigest())
162 if os.path.exists(os.path.join(tmpdir, messagename)):
163 time.sleep(2)
164 attempts += 1
165 else:
166 break
167 tmpmessagename = messagename.split(',')[0]
168 ui.debug('maildir', 'savemessage: using temporary name %s' % tmpmessagename)
169 file = open(os.path.join(tmpdir, tmpmessagename), "wt")
170 file.write(content)
171
172 # Make sure the data hits the disk
173 file.flush()
174 os.fsync(file.fileno())
175
176 file.close()
177 if rtime != None:
178 os.utime(os.path.join(tmpdir,tmpmessagename), (rtime,rtime))
179 ui.debug('maildir', 'savemessage: moving from %s to %s' % \
180 (tmpmessagename, messagename))
181 if tmpmessagename != messagename: # then rename it
182 os.link(os.path.join(tmpdir, tmpmessagename),
183 os.path.join(tmpdir, messagename))
184 os.unlink(os.path.join(tmpdir, tmpmessagename))
185
186 try:
187 # fsync the directory (safer semantics in Linux)
188 fd = os.open(tmpdir, os.O_RDONLY)
189 os.fsync(fd)
190 os.close(fd)
191 except:
192 pass
193
194 self.messagelist[uid] = {'uid': uid, 'flags': [],
195 'filename': os.path.join(tmpdir, messagename)}
196 self.savemessageflags(uid, flags)
197 ui.debug('maildir', 'savemessage: returning uid %d' % uid)
198 return uid
199
200 def getmessageflags(self, uid):
201 return self.messagelist[uid]['flags']
202
203 def savemessageflags(self, uid, flags):
204 oldfilename = self.messagelist[uid]['filename']
205 newpath, newname = os.path.split(oldfilename)
206 tmpdir = os.path.join(self.getfullname(), 'tmp')
207 if 'S' in flags:
208 # If a message has been seen, it goes into the cur
209 # directory. CR debian#152482, [complete.org #4]
210 newpath = os.path.join(self.getfullname(), 'cur')
211 else:
212 newpath = os.path.join(self.getfullname(), 'new')
213 infostr = ':'
214 infomatch = re.search('(:.*)$', newname)
215 if infomatch: # If the info string is present..
216 infostr = infomatch.group(1)
217 newname = newname.split(':')[0] # Strip off the info string.
218 infostr = re.sub('2,[A-Z]*', '', infostr)
219 flags.sort()
220 infostr += '2,' + ''.join(flags)
221 newname += infostr
222
223 newfilename = os.path.join(newpath, newname)
224 if (newfilename != oldfilename):
225 os.rename(oldfilename, newfilename)
226 self.messagelist[uid]['flags'] = flags
227 self.messagelist[uid]['filename'] = newfilename
228
229 # By now, the message had better not be in tmp/ land!
230 final_dir, final_name = os.path.split(self.messagelist[uid]['filename'])
231 assert final_dir != tmpdir
232
233 def deletemessage(self, uid):
234 if not uid in self.messagelist:
235 return
236 filename = self.messagelist[uid]['filename']
237 try:
238 os.unlink(filename)
239 except OSError:
240 # Can't find the file -- maybe already deleted?
241 newmsglist = self._scanfolder()
242 if uid in newmsglist: # Nope, try new filename.
243 os.unlink(newmsglist[uid]['filename'])
244 # Yep -- return.
245 del(self.messagelist[uid])
246