]> code.delx.au - offlineimap/blob - offlineimap/folder/Maildir.py
9b1b9ca3474b040dc2bc4421e3e4a5efe297f9a8
[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")
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 try:
202 # fsync the directory (safer semantics in Linux)
203 fd = os.open(tmpdir, os.O_RDONLY)
204 os.fsync(fd)
205 os.close(fd)
206 except:
207 pass
208
209 self.messagelist[uid] = {'uid': uid, 'flags': [],
210 'filename': os.path.join(tmpdir, messagename)}
211 self.savemessageflags(uid, flags)
212 ui.debug('maildir', 'savemessage: returning uid %d' % uid)
213 return uid
214
215 def getmessageflags(self, uid):
216 return self.messagelist[uid]['flags']
217
218 def savemessageflags(self, uid, flags):
219 oldfilename = self.messagelist[uid]['filename']
220 newpath, newname = os.path.split(oldfilename)
221 tmpdir = os.path.join(self.getfullname(), 'tmp')
222 if 'S' in flags:
223 # If a message has been seen, it goes into the cur
224 # directory. CR debian#152482, [complete.org #4]
225 newpath = os.path.join(self.getfullname(), 'cur')
226 else:
227 newpath = os.path.join(self.getfullname(), 'new')
228 infostr = ':'
229 infomatch = re.search('(:.*)$', newname)
230 if infomatch: # If the info string is present..
231 infostr = infomatch.group(1)
232 newname = newname.split(':')[0] # Strip off the info string.
233 infostr = re.sub('2,[A-Z]*', '', infostr)
234 flags.sort()
235 infostr += '2,' + ''.join(flags)
236 newname += infostr
237
238 newfilename = os.path.join(newpath, newname)
239 if (newfilename != oldfilename):
240 os.rename(oldfilename, newfilename)
241 self.messagelist[uid]['flags'] = flags
242 self.messagelist[uid]['filename'] = newfilename
243
244 # By now, the message had better not be in tmp/ land!
245 final_dir, final_name = os.path.split(self.messagelist[uid]['filename'])
246 assert final_dir != tmpdir
247
248 def deletemessage(self, uid):
249 if not uid in self.messagelist:
250 return
251 filename = self.messagelist[uid]['filename']
252 try:
253 os.unlink(filename)
254 except OSError:
255 # Can't find the file -- maybe already deleted?
256 newmsglist = self._scanfolder()
257 if uid in newmsglist: # Nope, try new filename.
258 os.unlink(newmsglist[uid]['filename'])
259 # Yep -- return.
260 del(self.messagelist[uid])
261