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