]> code.delx.au - offlineimap/blob - offlineimap/folder/Maildir.py
Configurable thread status character for ui.Curses.Blinkenlights
[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 if rtime != None:
174 os.utime(os.path.join(tmpdir,tmpmessagename), (rtime,rtime))
175 ui.debug('maildir', 'savemessage: moving from %s to %s' % \
176 (tmpmessagename, messagename))
177 os.link(os.path.join(tmpdir, tmpmessagename),
178 os.path.join(newdir, messagename))
179 os.unlink(os.path.join(tmpdir, tmpmessagename))
180 self.messagelist[uid] = {'uid': uid, 'flags': [],
181 'filename': os.path.join(newdir, messagename)}
182 self.savemessageflags(uid, flags)
183 ui.debug('maildir', 'savemessage: returning uid %d' % uid)
184 return uid
185
186 def getmessageflags(self, uid):
187 return self.messagelist[uid]['flags']
188
189 def savemessageflags(self, uid, flags):
190 oldfilename = self.messagelist[uid]['filename']
191 newpath, newname = os.path.split(oldfilename)
192 if 'S' in flags:
193 # If a message has been seen, it goes into the cur
194 # directory. CR debian#152482, [complete.org #4]
195 newpath = os.path.join(self.getfullname(), 'cur')
196 else:
197 newpath = os.path.join(self.getfullname(), 'new')
198 infostr = ':'
199 infomatch = re.search('(:.*)$', newname)
200 if infomatch: # If the info string is present..
201 infostr = infomatch.group(1)
202 newname = newname.split(':')[0] # Strip off the info string.
203 infostr = re.sub('2,[A-Z]*', '', infostr)
204 flags.sort()
205 infostr += '2,' + ''.join(flags)
206 newname += infostr
207
208 newfilename = os.path.join(newpath, newname)
209 if (newfilename != oldfilename):
210 os.rename(oldfilename, newfilename)
211 self.messagelist[uid]['flags'] = flags
212 self.messagelist[uid]['filename'] = newfilename
213
214 def deletemessage(self, uid):
215 if not uid in self.messagelist:
216 return
217 filename = self.messagelist[uid]['filename']
218 try:
219 os.unlink(filename)
220 except OSError:
221 # Can't find the file -- maybe already deleted?
222 newmsglist = self._scanfolder()
223 if uid in newmsglist: # Nope, try new filename.
224 os.unlink(newmsglist[uid]['filename'])
225 # Yep -- return.
226 del(self.messagelist[uid])
227