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