]> code.delx.au - offlineimap/blob - offlineimap/folder/Maildir.py
c5d509282c18ece03fe560391245e0ababbebf83
[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 file = fd = None
162 messagename = tmpmessaename = None
163 attempts = 0
164 while 1:
165 if attempts > 15:
166 raise IOError, "Couldn't write to file %s" % messagename
167 timeval, timeseq = gettimeseq()
168 messagename = '%d_%d.%d.%s,U=%d,FMD5=%s' % \
169 (timeval,
170 timeseq,
171 os.getpid(),
172 socket.gethostname(),
173 uid,
174 md5.new(self.getvisiblename()).hexdigest())
175 tmpmessagename = messagename.split(',')[0]
176 try:
177 fd = os.open(os.path.join(tmpdir, tmpmessagename),
178 os.O_WRONLY + os.O_CREAT + os.O_EXCL)
179 file = os.fdopen(fd, 'w')
180 ui.debug('maildir', 'savemessage: using temporary name %s' % tmpmessagename)
181 except OSError, e:
182 if e.errno == 17:
183 time.sleep(2)
184 attempts += 1
185 continue
186 raise
187
188 file.write(content)
189
190 # Make sure the data hits the disk
191 file.flush()
192 os.fsync(fd)
193
194 file.close()
195 if rtime != None:
196 os.utime(os.path.join(tmpdir,tmpmessagename), (rtime,rtime))
197 ui.debug('maildir', 'savemessage: moving from %s to %s' % \
198 (tmpmessagename, messagename))
199 if tmpmessagename != messagename: # then rename it
200 os.link(os.path.join(tmpdir, tmpmessagename),
201 os.path.join(tmpdir, messagename))
202 os.unlink(os.path.join(tmpdir, tmpmessagename))
203
204 try:
205 # fsync the directory (safer semantics in Linux)
206 fd = os.open(tmpdir, os.O_RDONLY)
207 os.fsync(fd)
208 os.close(fd)
209 except:
210 pass
211
212 self.messagelist[uid] = {'uid': uid, 'flags': [],
213 'filename': os.path.join(tmpdir, messagename)}
214 self.savemessageflags(uid, flags)
215 ui.debug('maildir', 'savemessage: returning uid %d' % uid)
216 return uid
217
218 def getmessageflags(self, uid):
219 return self.messagelist[uid]['flags']
220
221 def savemessageflags(self, uid, flags):
222 oldfilename = self.messagelist[uid]['filename']
223 newpath, newname = os.path.split(oldfilename)
224 tmpdir = os.path.join(self.getfullname(), 'tmp')
225 if 'S' in flags:
226 # If a message has been seen, it goes into the cur
227 # directory. CR debian#152482, [complete.org #4]
228 newpath = os.path.join(self.getfullname(), 'cur')
229 else:
230 newpath = os.path.join(self.getfullname(), 'new')
231 infostr = ':'
232 infomatch = re.search('(:.*)$', newname)
233 if infomatch: # If the info string is present..
234 infostr = infomatch.group(1)
235 newname = newname.split(':')[0] # Strip off the info string.
236 infostr = re.sub('2,[A-Z]*', '', infostr)
237 flags.sort()
238 infostr += '2,' + ''.join(flags)
239 newname += infostr
240
241 newfilename = os.path.join(newpath, newname)
242 if (newfilename != oldfilename):
243 os.rename(oldfilename, newfilename)
244 self.messagelist[uid]['flags'] = flags
245 self.messagelist[uid]['filename'] = newfilename
246
247 # By now, the message had better not be in tmp/ land!
248 final_dir, final_name = os.path.split(self.messagelist[uid]['filename'])
249 assert final_dir != tmpdir
250
251 def deletemessage(self, uid):
252 if not uid in self.messagelist:
253 return
254 filename = self.messagelist[uid]['filename']
255 try:
256 os.unlink(filename)
257 except OSError:
258 # Can't find the file -- maybe already deleted?
259 newmsglist = self._scanfolder()
260 if uid in newmsglist: # Nope, try new filename.
261 os.unlink(newmsglist[uid]['filename'])
262 # Yep -- return.
263 del(self.messagelist[uid])
264