]>
code.delx.au - offlineimap/blob - offlineimap/folder/Maildir.py
1 # Maildir folder support
2 # Copyright (C) 2002 - 2007 John Goerzen
3 # <jgoerzen@complete.org>
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.
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.
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
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
25 uidmatchre
= re
.compile(',U=(\d+)')
26 flagmatchre
= re
.compile(':.*2,([A-Z]+)')
33 global lasttime
, timeseq
, timelock
36 thistime
= long(time
.time())
37 if thistime
== lasttime
:
39 return (thistime
, timeseq
)
43 return (thistime
, timeseq
)
47 class MaildirFolder(BaseFolder
):
48 def __init__(self
, root
, name
, sep
, repository
, accountname
):
52 self
.messagelist
= None
53 self
.repository
= repository
54 self
.accountname
= accountname
55 BaseFolder
.__init
__(self
)
57 def getaccountname(self
):
58 return self
.accountname
60 def getfullname(self
):
61 return os
.path
.join(self
.getroot(), self
.getname())
63 def getuidvalidity(self
):
64 """Maildirs have no notion of uidvalidity, so we just return a magic
68 def _scanfolder(self
):
69 """Cache the message list. Maildir flags are:
75 and must occur in ASCII order."""
78 nouidcounter
= -1 # Messages without UIDs get
79 # negative UID numbers.
80 foldermd5
= md5
.new(self
.getvisiblename()).hexdigest()
81 folderstr
= ',FMD5=' + foldermd5
82 for dirannex
in ['new', 'cur']:
83 fulldirname
= os
.path
.join(self
.getfullname(), dirannex
)
84 files
.extend([os
.path
.join(fulldirname
, filename
) for
85 filename
in os
.listdir(fulldirname
)])
87 messagename
= os
.path
.basename(file)
88 foldermatch
= messagename
.find(folderstr
) != -1
90 # If there is no folder MD5 specified, or if it mismatches,
91 # assume it is a foreign (new) message and generate a
95 else: # It comes from our folder.
96 uidmatch
= uidmatchre
.search(messagename
)
102 uid
= long(uidmatch
.group(1))
103 flagmatch
= flagmatchre
.search(messagename
)
106 flags
= [x
for x
in flagmatch
.group(1)]
108 retval
[uid
] = {'uid': uid
,
113 def quickchanged(self
, statusfolder
):
114 self
.cachemessagelist()
115 savedmessages
= statusfolder
.getmessagelist()
116 if len(self
.messagelist
) != len(savedmessages
):
118 for uid
in self
.messagelist
.keys():
119 if uid
not in savedmessages
:
121 if self
.messagelist
[uid
]['flags'] != savedmessages
[uid
]['flags']:
125 def cachemessagelist(self
):
126 if self
.messagelist
is None:
127 self
.messagelist
= self
._scanfolder
()
129 def getmessagelist(self
):
130 return self
.messagelist
132 def getmessage(self
, uid
):
133 filename
= self
.messagelist
[uid
]['filename']
134 file = open(filename
, 'rt')
137 return retval
.replace("\r\n", "\n")
139 def getmessagetime( self
, uid
):
140 filename
= self
.messagelist
[uid
]['filename']
141 st
= os
.stat(filename
)
144 def savemessage(self
, uid
, content
, flags
, rtime
):
145 # This function only ever saves to tmp/,
146 # but it calls savemessageflags() to actually save to cur/ or new/.
147 ui
= UIBase
.getglobalui()
148 ui
.debug('maildir', 'savemessage: called to write with flags %s and content %s' % \
149 (repr(flags
), repr(content
)))
151 # We cannot assign a new uid.
153 if uid
in self
.messagelist
:
154 # We already have it.
155 self
.savemessageflags(uid
, flags
)
158 # Otherwise, save the message in tmp/ and then call savemessageflags()
159 # to give it a permanent home.
160 tmpdir
= os
.path
.join(self
.getfullname(), 'tmp')
165 raise IOError, "Couldn't write to file %s" % messagename
166 timeval
, timeseq
= gettimeseq()
167 messagename
= '%d_%d.%d.%s,U=%d,FMD5=%s' % \
171 socket
.gethostname(),
173 md5
.new(self
.getvisiblename()).hexdigest())
174 if os
.path
.exists(os
.path
.join(tmpdir
, messagename
)):
179 tmpmessagename
= messagename
.split(',')[0]
180 ui
.debug('maildir', 'savemessage: using temporary name %s' % tmpmessagename
)
181 file = open(os
.path
.join(tmpdir
, tmpmessagename
), "wt")
184 # Make sure the data hits the disk
186 os
.fsync(file.fileno())
190 os
.utime(os
.path
.join(tmpdir
,tmpmessagename
), (rtime
,rtime
))
191 ui
.debug('maildir', 'savemessage: moving from %s to %s' % \
192 (tmpmessagename
, messagename
))
193 if tmpmessagename
!= messagename
: # then rename it
194 os
.link(os
.path
.join(tmpdir
, tmpmessagename
),
195 os
.path
.join(tmpdir
, messagename
))
196 os
.unlink(os
.path
.join(tmpdir
, tmpmessagename
))
199 # fsync the directory (safer semantics in Linux)
200 fd
= os
.open(tmpdir
, os
.O_RDONLY
)
206 self
.messagelist
[uid
] = {'uid': uid
, 'flags': [],
207 'filename': os
.path
.join(tmpdir
, messagename
)}
208 self
.savemessageflags(uid
, flags
)
209 ui
.debug('maildir', 'savemessage: returning uid %d' % uid
)
212 def getmessageflags(self
, uid
):
213 return self
.messagelist
[uid
]['flags']
215 def savemessageflags(self
, uid
, flags
):
216 oldfilename
= self
.messagelist
[uid
]['filename']
217 newpath
, newname
= os
.path
.split(oldfilename
)
218 tmpdir
= os
.path
.join(self
.getfullname(), 'tmp')
220 # If a message has been seen, it goes into the cur
221 # directory. CR debian#152482, [complete.org #4]
222 newpath
= os
.path
.join(self
.getfullname(), 'cur')
224 newpath
= os
.path
.join(self
.getfullname(), 'new')
226 infomatch
= re
.search('(:.*)$', newname
)
227 if infomatch
: # If the info string is present..
228 infostr
= infomatch
.group(1)
229 newname
= newname
.split(':')[0] # Strip off the info string.
230 infostr
= re
.sub('2,[A-Z]*', '', infostr
)
232 infostr
+= '2,' + ''.join(flags
)
235 newfilename
= os
.path
.join(newpath
, newname
)
236 if (newfilename
!= oldfilename
):
237 os
.rename(oldfilename
, newfilename
)
238 self
.messagelist
[uid
]['flags'] = flags
239 self
.messagelist
[uid
]['filename'] = newfilename
241 # By now, the message had better not be in tmp/ land!
242 final_dir
, final_name
= os
.path
.split(self
.messagelist
[uid
]['filename'])
243 assert final_dir
!= tmpdir
245 def deletemessage(self
, uid
):
246 if not uid
in self
.messagelist
:
248 filename
= self
.messagelist
[uid
]['filename']
252 # Can't find the file -- maybe already deleted?
253 newmsglist
= self
._scanfolder
()
254 if uid
in newmsglist
: # Nope, try new filename.
255 os
.unlink(newmsglist
[uid
]['filename'])
257 del(self
.messagelist
[uid
])