]> code.delx.au - offlineimap/blob - offlineimap/folder/Maildir.py
Update FSF address
[offlineimap] / offlineimap / folder / Maildir.py
1 # Maildir folder support
2 # Copyright (C) 2002 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 savemessage(self, uid, content, flags):
128 ui = UIBase.getglobalui()
129 ui.debug('maildir', 'savemessage: called to write with flags %s and content %s' % \
130 (repr(flags), repr(content)))
131 if uid < 0:
132 # We cannot assign a new uid.
133 return uid
134 if uid in self.messagelist:
135 # We already have it.
136 self.savemessageflags(uid, flags)
137 return uid
138 if 'S' in flags:
139 # If a message has been seen, it goes into the cur
140 # directory. CR debian#152482, [complete.org #4]
141 newdir = os.path.join(self.getfullname(), 'cur')
142 else:
143 newdir = os.path.join(self.getfullname(), 'new')
144 tmpdir = os.path.join(self.getfullname(), 'tmp')
145 messagename = None
146 attempts = 0
147 while 1:
148 if attempts > 15:
149 raise IOError, "Couldn't write to file %s" % messagename
150 timeval, timeseq = gettimeseq()
151 messagename = '%d_%d.%d.%s,U=%d,FMD5=%s' % \
152 (timeval,
153 timeseq,
154 os.getpid(),
155 socket.gethostname(),
156 uid,
157 md5.new(self.getvisiblename()).hexdigest())
158 if os.path.exists(os.path.join(tmpdir, messagename)):
159 time.sleep(2)
160 attempts += 1
161 else:
162 break
163 tmpmessagename = messagename.split(',')[0]
164 ui.debug('maildir', 'savemessage: using temporary name %s' % tmpmessagename)
165 file = open(os.path.join(tmpdir, tmpmessagename), "wt")
166 file.write(content)
167 file.close()
168 ui.debug('maildir', 'savemessage: moving from %s to %s' % \
169 (tmpmessagename, messagename))
170 os.link(os.path.join(tmpdir, tmpmessagename),
171 os.path.join(newdir, messagename))
172 os.unlink(os.path.join(tmpdir, tmpmessagename))
173 self.messagelist[uid] = {'uid': uid, 'flags': [],
174 'filename': os.path.join(newdir, messagename)}
175 self.savemessageflags(uid, flags)
176 ui.debug('maildir', 'savemessage: returning uid %d' % uid)
177 return uid
178
179 def getmessageflags(self, uid):
180 return self.messagelist[uid]['flags']
181
182 def savemessageflags(self, uid, flags):
183 oldfilename = self.messagelist[uid]['filename']
184 newpath, newname = os.path.split(oldfilename)
185 if 'S' in flags:
186 # If a message has been seen, it goes into the cur
187 # directory. CR debian#152482, [complete.org #4]
188 newpath = os.path.join(self.getfullname(), 'cur')
189 else:
190 newpath = os.path.join(self.getfullname(), 'new')
191 infostr = ':'
192 infomatch = re.search('(:.*)$', newname)
193 if infomatch: # If the info string is present..
194 infostr = infomatch.group(1)
195 newname = newname.split(':')[0] # Strip off the info string.
196 infostr = re.sub('2,[A-Z]*', '', infostr)
197 flags.sort()
198 infostr += '2,' + ''.join(flags)
199 newname += infostr
200
201 newfilename = os.path.join(newpath, newname)
202 if (newfilename != oldfilename):
203 os.rename(oldfilename, newfilename)
204 self.messagelist[uid]['flags'] = flags
205 self.messagelist[uid]['filename'] = newfilename
206
207 def deletemessage(self, uid):
208 if not uid in self.messagelist:
209 return
210 filename = self.messagelist[uid]['filename']
211 try:
212 os.unlink(filename)
213 except OSError:
214 # Can't find the file -- maybe already deleted?
215 newmsglist = self._scanfolder()
216 if uid in newmsglist: # Nope, try new filename.
217 os.unlink(newmsglist[uid]['filename'])
218 # Yep -- return.
219 del(self.messagelist[uid])
220