@@ -395,6 +395,56 @@ def get_file(self, key):
395395 f = open (os .path .join (self ._path , self ._lookup (key )), 'rb' )
396396 return _ProxyFile (f )
397397
398+ def get_info (self , key ):
399+ """Get the keyed message's "info" as a string."""
400+ subpath = self ._lookup (key )
401+ if self .colon in subpath :
402+ return subpath .split (self .colon )[- 1 ]
403+ return ''
404+
405+ def set_info (self , key , info : str ):
406+ """Set the keyed message's "info" string."""
407+ if not isinstance (info , str ):
408+ raise TypeError (f'info must be a string: { type (info )} ' )
409+ old_subpath = self ._lookup (key )
410+ new_subpath = old_subpath .split (self .colon )[0 ]
411+ if info :
412+ new_subpath += self .colon + info
413+ if new_subpath == old_subpath :
414+ return
415+ old_path = os .path .join (self ._path , old_subpath )
416+ new_path = os .path .join (self ._path , new_subpath )
417+ os .rename (old_path , new_path )
418+ self ._toc [key ] = new_subpath
419+
420+ def get_flags (self , key ):
421+ """Return as a string the standard flags that are set on the keyed message."""
422+ info = self .get_info (key )
423+ if info .startswith ('2,' ):
424+ return info [2 :]
425+ return ''
426+
427+ def set_flags (self , key , flags : str ):
428+ """Set the given flags and unset all others on the keyed message."""
429+ if not isinstance (flags , str ):
430+ raise TypeError (f'flags must be a string: { type (flags )} ' )
431+ # TODO: check if flags are valid standard flag characters?
432+ self .set_info (key , '2,' + '' .join (sorted (set (flags ))))
433+
434+ def add_flag (self , key , flag : str ):
435+ """Set the given flag(s) without changing others on the keyed message."""
436+ if not isinstance (flag , str ):
437+ raise TypeError (f'flag must be a string: { type (flag )} ' )
438+ # TODO: check that flag is a valid standard flag character?
439+ self .set_flags (key , '' .join (set (self .get_flags (key )) | set (flag )))
440+
441+ def remove_flag (self , key , flag : str ):
442+ """Unset the given string flag(s) without changing others on the keyed message."""
443+ if not isinstance (flag , str ):
444+ raise TypeError (f'flag must be a string: { type (flag )} ' )
445+ if self .get_flags (key ):
446+ self .set_flags (key , '' .join (set (self .get_flags (key )) - set (flag )))
447+
398448 def iterkeys (self ):
399449 """Return an iterator over keys."""
400450 self ._refresh ()
@@ -540,6 +590,8 @@ def _refresh(self):
540590 for subdir in self ._toc_mtimes :
541591 path = self ._paths [subdir ]
542592 for entry in os .listdir (path ):
593+ if entry .startswith ('.' ):
594+ continue
543595 p = os .path .join (path , entry )
544596 if os .path .isdir (p ):
545597 continue
@@ -698,9 +750,13 @@ def flush(self):
698750 _sync_close (new_file )
699751 # self._file is about to get replaced, so no need to sync.
700752 self ._file .close ()
701- # Make sure the new file's mode is the same as the old file's
702- mode = os .stat (self ._path ).st_mode
703- os .chmod (new_file .name , mode )
753+ # Make sure the new file's mode and owner are the same as the old file's
754+ info = os .stat (self ._path )
755+ os .chmod (new_file .name , info .st_mode )
756+ try :
757+ os .chown (new_file .name , info .st_uid , info .st_gid )
758+ except (AttributeError , OSError ):
759+ pass
704760 try :
705761 os .rename (new_file .name , self ._path )
706762 except FileExistsError :
@@ -778,10 +834,11 @@ def get_message(self, key):
778834 """Return a Message representation or raise a KeyError."""
779835 start , stop = self ._lookup (key )
780836 self ._file .seek (start )
781- from_line = self ._file .readline ().replace (linesep , b'' )
837+ from_line = self ._file .readline ().replace (linesep , b'' ). decode ( 'ascii' )
782838 string = self ._file .read (stop - self ._file .tell ())
783839 msg = self ._message_factory (string .replace (linesep , b'\n ' ))
784- msg .set_from (from_line [5 :].decode ('ascii' ))
840+ msg .set_unixfrom (from_line )
841+ msg .set_from (from_line [5 :])
785842 return msg
786843
787844 def get_string (self , key , from_ = False ):
@@ -1089,10 +1146,24 @@ def __len__(self):
10891146 """Return a count of messages in the mailbox."""
10901147 return len (list (self .iterkeys ()))
10911148
1149+ def _open_mh_sequences_file (self , text ):
1150+ mode = '' if text else 'b'
1151+ kwargs = {'encoding' : 'ASCII' } if text else {}
1152+ path = os .path .join (self ._path , '.mh_sequences' )
1153+ while True :
1154+ try :
1155+ return open (path , 'r+' + mode , ** kwargs )
1156+ except FileNotFoundError :
1157+ pass
1158+ try :
1159+ return open (path , 'x+' + mode , ** kwargs )
1160+ except FileExistsError :
1161+ pass
1162+
10921163 def lock (self ):
10931164 """Lock the mailbox."""
10941165 if not self ._locked :
1095- self ._file = open ( os . path . join ( self ._path , '.mh_sequences' ), 'rb+' )
1166+ self ._file = self ._open_mh_sequences_file ( text = False )
10961167 _lock_file (self ._file )
10971168 self ._locked = True
10981169
@@ -1146,7 +1217,11 @@ def remove_folder(self, folder):
11461217 def get_sequences (self ):
11471218 """Return a name-to-key-list dictionary to define each sequence."""
11481219 results = {}
1149- with open (os .path .join (self ._path , '.mh_sequences' ), 'r' , encoding = 'ASCII' ) as f :
1220+ try :
1221+ f = open (os .path .join (self ._path , '.mh_sequences' ), 'r' , encoding = 'ASCII' )
1222+ except FileNotFoundError :
1223+ return results
1224+ with f :
11501225 all_keys = set (self .keys ())
11511226 for line in f :
11521227 try :
@@ -1169,7 +1244,7 @@ def get_sequences(self):
11691244
11701245 def set_sequences (self , sequences ):
11711246 """Set sequences using the given name-to-key-list dictionary."""
1172- f = open ( os . path . join ( self ._path , '.mh_sequences' ), 'r+' , encoding = 'ASCII' )
1247+ f = self ._open_mh_sequences_file ( text = True )
11731248 try :
11741249 os .close (os .open (f .name , os .O_WRONLY | os .O_TRUNC ))
11751250 for name , keys in sequences .items ():
@@ -1956,10 +2031,7 @@ def readlines(self, sizehint=None):
19562031
19572032 def __iter__ (self ):
19582033 """Iterate over lines."""
1959- while True :
1960- line = self .readline ()
1961- if not line :
1962- return
2034+ while line := self .readline ():
19632035 yield line
19642036
19652037 def tell (self ):
0 commit comments