]> code.delx.au - offlineimap/blob - offlineimap/folder/Maildir.py
Update copyright date in Maildir.py
[offlineimap] / offlineimap / folder / Maildir.py
1 # Maildir folder support
2 # Copyright (C) 2002 - 2006 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 foldermatchre = re.compile(',FMD5=([0-9a-f]{32})')
26 uidmatchre = re.compile(',U=(\d+)')
27 flagmatchre = re.compile(':.*2,([A-Z]+)')
28
29 timeseq = 0
30 lasttime = long(0)
31 timelock = Lock()
32
33 def gettimeseq():
34 global lasttime, timeseq, timelock
35 timelock.acquire()
36 try:
37 thistime = long(time.time())
38 if thistime == lasttime:
39 timeseq += 1
40 return (thistime, timeseq)
41 else:
42 lasttime = thistime
43 timeseq = 0
44 return (thistime, timeseq)
45 finally:
46 timelock.release()
47
48 class MaildirFolder(BaseFolder):
49 def __init__(self, root, name, sep, repository, accountname):
50 self.name = name
51 self.root = root
52 self.sep = sep
53 self.messagelist = None
54 self.repository = repository
55 self.accountname = accountname
56 BaseFolder.__init__(self)
57
58 def getaccountname(self):
59 return self.accountname
60
61 def getfullname(self):
62 return os.path.join(self.getroot(), self.getname())
63
64 def getuidvalidity(self):
65 """Maildirs have no notion of uidvalidity, so we just return a magic
66 token."""
67 return 42
68
69 def _scanfolder(self):
70 """Cache the message list. Maildir flags are:
71 R (replied)
72 S (seen)
73 T (trashed)
74 D (draft)
75 F (flagged)
76 and must occur in ASCII order."""
77 retval = {}
78 files = []
79 nouidcounter = -1 # Messages without UIDs get
80 # negative UID numbers.
81 for dirannex in ['new', 'cur']:
82 fulldirname = os.path.join(self.getfullname(), dirannex)
83 files.extend([os.path.join(fulldirname, filename) for
84 filename in os.listdir(fulldirname)])
85 for file in files:
86 messagename = os.path.basename(file)
87 foldermatch = foldermatchre.search(messagename)
88 if (not foldermatch) or \
89 md5.new(self.getvisiblename()).hexdigest() \
90 != foldermatch.group(1):
91 # If there is no folder MD5 specified, or if it mismatches,
92 # assume it is a foreign (new) message and generate a
93 # negative uid for it
94 uid = nouidcounter
95 nouidcounter -= 1
96 else: # It comes from our folder.
97 uidmatch = uidmatchre.search(messagename)
98 uid = None
99 if not uidmatch:
100 uid = nouidcounter
101 nouidcounter -= 1
102 else:
103 uid = long(uidmatch.group(1))
104 flagmatch = flagmatchre.search(messagename)
105 flags = []
106 if flagmatch:
107 flags = [x for x in flagmatch.group(1)]
108 flags.sort()
109 retval[uid] = {'uid': uid,
110 'flags': flags,
111 'filename': file}
112 return retval
113
114 def cachemessagelist(self):
115 self.messagelist = self._scanfolder()
116
117 def getmessagelist(self):
118 return self.messagelist
119
120 def getmessage(self, uid):
121 filename = self.messagelist[uid]['filename']
122 file = open(filename, 'rt')
123 retval = file.read()
124 file.close()
125 return retval.replace("\r\n", "\n")
126
127 def getmessagetime( self, uid ):
128 filename = self.messagelist[uid]['filename']
129 st = os.stat(filename)
130 return st.st_mtime
131
132 def savemessage(self, uid, content, flags, rtime):
133 ui = UIBase.getglobalui()
134 ui.debug('maildir', 'savemessage: called to write with flags %s and content %s' % \
135 (repr(flags), repr(content)))
136 if uid < 0:
137 # We cannot assign a new uid.
138 return uid
139 if uid in self.messagelist:
140 # We already have it.
141 self.savemessageflags(uid, flags)
142 return uid
143 if 'S' in flags:
144 # If a message has been seen, it goes into the cur
145 # directory. CR debian#152482, [complete.org #4]
146 newdir = os.path.join(self.getfullname(), 'cur')
147 else:
148 newdir = os.path.join(self.getfullname(), 'new')
149 tmpdir = os.path.join(self.getfullname(), 'tmp')
150 messagename = None
151 attempts = 0
152 while 1:
153 if attempts > 15:
154 raise IOError, "Couldn't write to file %s" % messagename
155 timeval, timeseq = gettimeseq()
156 messagename = '%d_%d.%d.%s,U=%d,FMD5=%s' % \
157 (timeval,
158 timeseq,
159 os.getpid(),
160 socket.gethostname(),
161 uid,
162 md5.new(self.getvisiblename()).hexdigest())
163 if os.path.exists(os.path.join(tmpdir, messagename)):
164 time.sleep(2)
165 attempts += 1
166 else:
167 break
168 tmpmessagename = messagename.split(',')[0]
169 ui.debug('maildir', 'savemessage: using temporary name %s' % tmpmessagename)
170 file = open(os.path.join(tmpdir, tmpmessagename), "wt")
171 file.write(content)
172 file.close()
173 os.utime(os.path.join(tmpdir,tmpmessagename), (rtime,rtime))
174 ui.debug('maildir', 'savemessage: moving from %s to %s' % \
175 (tmpmessagename, messagename))
176 os.link(os.path.join(tmpdir, tmpmessagename),
177 os.path.join(newdir, messagename))
178 os.unlink(os.path.join(tmpdir, tmpmessagename))
179 self.messagelist[uid] = {'uid': uid, 'flags': [],
180 'filename': os.path.join(newdir, messagename)}
181 self.savemessageflags(uid, flags)
182 ui.debug('maildir', 'savemessage: returning uid %d' % uid)
183 return uid
184
185 def getmessageflags(self, uid):
186 return self.messagelist[uid]['flags']
187
188 def savemessageflags(self, uid, flags):
189 oldfilename = self.messagelist[uid]['filename']
190 newpath, newname = os.path.split(oldfilename)
191 if 'S' in flags:
192 # If a message has been seen, it goes into the cur
193 # directory. CR debian#152482, [complete.org #4]
194 newpath = os.path.join(self.getfullname(), 'cur')
195 else:
196 newpath = os.path.join(self.getfullname(), 'new')
197 infostr = ':'
198 infomatch = re.search('(:.*)$', newname)
199 if infomatch: # If the info string is present..
200 infostr = infomatch.group(1)
201 newname = newname.split(':')[0] # Strip off the info string.
202 infostr = re.sub('2,[A-Z]*', '', infostr)
203 flags.sort()
204 infostr += '2,' + ''.join(flags)
205 newname += infostr
206
207 newfilename = os.path.join(newpath, newname)
208 if (newfilename != oldfilename):
209 os.rename(oldfilename, newfilename)
210 self.messagelist[uid]['flags'] = flags
211 self.messagelist[uid]['filename'] = newfilename
212
213 def deletemessage(self, uid):
214 if not uid in self.messagelist:
215 return
216 filename = self.messagelist[uid]['filename']
217 try:
218 os.unlink(filename)
219 except OSError:
220 # Can't find the file -- maybe already deleted?
221 newmsglist = self._scanfolder()
222 if uid in newmsglist: # Nope, try new filename.
223 os.unlink(newmsglist[uid]['filename'])
224 # Yep -- return.
225 del(self.messagelist[uid])
226