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