]>
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 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
26 from hashlib
import md5
30 uidmatchre
= re
.compile(',U=(\d+)')
31 flagmatchre
= re
.compile(':.*2,([A-Z]+)')
38 global lasttime
, timeseq
, timelock
41 thistime
= long(time
.time())
42 if thistime
== lasttime
:
44 return (thistime
, timeseq
)
48 return (thistime
, timeseq
)
52 class MaildirFolder(BaseFolder
):
53 def __init__(self
, root
, name
, sep
, repository
, accountname
, config
):
56 self
.dofsync
= config
.getdefaultboolean("general", "fsync", True)
59 self
.messagelist
= None
60 self
.repository
= repository
61 self
.accountname
= accountname
62 BaseFolder
.__init
__(self
)
64 def getaccountname(self
):
65 return self
.accountname
67 def getfullname(self
):
68 return os
.path
.join(self
.getroot(), self
.getname())
70 def getuidvalidity(self
):
71 """Maildirs have no notion of uidvalidity, so we just return a magic
75 def _scanfolder(self
):
76 """Cache the message list. Maildir flags are:
82 and must occur in ASCII order."""
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
)])
94 messagename
= os
.path
.basename(file)
95 foldermatch
= messagename
.find(folderstr
) != -1
97 # If there is no folder MD5 specified, or if it mismatches,
98 # assume it is a foreign (new) message and generate a
102 else: # It comes from our folder.
103 uidmatch
= uidmatchre
.search(messagename
)
109 uid
= long(uidmatch
.group(1))
110 flagmatch
= flagmatchre
.search(messagename
)
113 flags
= [x
for x
in flagmatch
.group(1)]
115 retval
[uid
] = {'uid': uid
,
120 def quickchanged(self
, statusfolder
):
121 self
.cachemessagelist()
122 savedmessages
= statusfolder
.getmessagelist()
123 if len(self
.messagelist
) != len(savedmessages
):
125 for uid
in self
.messagelist
.keys():
126 if uid
not in savedmessages
:
128 if self
.messagelist
[uid
]['flags'] != savedmessages
[uid
]['flags']:
132 def cachemessagelist(self
):
133 if self
.messagelist
is None:
134 self
.messagelist
= self
._scanfolder
()
136 def getmessagelist(self
):
137 return self
.messagelist
139 def getmessage(self
, uid
):
140 filename
= self
.messagelist
[uid
]['filename']
141 file = open(filename
, 'rt')
144 return retval
.replace("\r\n", "\n")
146 def getmessagetime( self
, uid
):
147 filename
= self
.messagelist
[uid
]['filename']
148 st
= os
.stat(filename
)
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
)))
158 # We cannot assign a new uid.
160 if uid
in self
.messagelist
:
161 # We already have it.
162 self
.savemessageflags(uid
, flags
)
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')
172 raise IOError, "Couldn't write to file %s" % messagename
173 timeval
, timeseq
= gettimeseq()
174 messagename
= '%d_%d.%d.%s,U=%d,FMD5=%s' % \
178 socket
.gethostname(),
180 md5(self
.getvisiblename()).hexdigest())
181 if os
.path
.exists(os
.path
.join(tmpdir
, messagename
)):
186 tmpmessagename
= messagename
.split(',')[0]
187 ui
.debug('maildir', 'savemessage: using temporary name %s' % tmpmessagename
)
188 file = open(os
.path
.join(tmpdir
, tmpmessagename
), "wt")
191 # Make sure the data hits the disk
194 os
.fsync(file.fileno())
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
))
208 # fsync the directory (safer semantics in Linux)
209 fd
= os
.open(tmpdir
, os
.O_RDONLY
)
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
)
221 def getmessageflags(self
, uid
):
222 return self
.messagelist
[uid
]['flags']
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')
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')
233 newpath
= os
.path
.join(self
.getfullname(), 'new')
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
)
241 infostr
+= '2,' + ''.join(flags
)
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
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
254 def deletemessage(self
, uid
):
255 if not uid
in self
.messagelist
:
257 filename
= self
.messagelist
[uid
]['filename']
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'])
266 del(self
.messagelist
[uid
])