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