1// The maildir package provides an interface to mailboxes in the Maildir format. 2// 3// Maildir mailboxes are designed to be safe for concurrent delivery. This 4// means that at the same time, multiple processes can deliver to the same 5// mailbox. However only one process can receive and read messages stored in 6// the Maildir. 7package maildir 8 9import ( 10 "crypto/rand" 11 "encoding/hex" 12 "io" 13 "os" 14 "path/filepath" 15 "sort" 16 "strconv" 17 "strings" 18 "sync/atomic" 19 "time" 20) 21 22// The separator separates a messages unique key from its flags in the filename. 23// This should only be changed on operating systems where the colon isn't 24// allowed in filenames. 25var separator rune = ':' 26 27var id int64 = 10000 28 29// createMode holds the permissions used when creating a directory. 30const createMode = 0700 31 32// A KeyError occurs when a key matches more or less than one message. 33type KeyError struct { 34 Key string // the (invalid) key 35 N int // number of matches (!= 1) 36} 37 38func (e *KeyError) Error() string { 39 return "maildir: key " + e.Key + " matches " + strconv.Itoa(e.N) + " files." 40} 41 42// A FlagError occurs when a non-standard info section is encountered. 43type FlagError struct { 44 Info string // the encountered info section 45 Experimental bool // info section starts with 1 46} 47 48func (e *FlagError) Error() string { 49 if e.Experimental { 50 return "maildir: experimental info section encountered: " + e.Info[2:] 51 } 52 return "maildir: bad info section encountered: " + e.Info 53} 54 55// A MailfileError occurs when a mailfile has an invalid format 56type MailfileError struct { 57 Name string // the name of the mailfile 58} 59 60func (e *MailfileError) Error() string { 61 return "maildir: invalid mailfile format: " + e.Name 62} 63 64// A Dir represents a single directory in a Maildir mailbox. 65// 66// Dir is used by programs receiving and reading messages from a Maildir. Only 67// one process can perform these operations. Programs which only need to 68// deliver new messages to the Maildir should use Delivery. 69type Dir string 70 71// Unseen moves messages from new to cur and returns their keys. 72// This means the messages are now known to the application. To find out whether 73// a user has seen a message, use Flags(). 74func (d Dir) Unseen() ([]string, error) { 75 f, err := os.Open(filepath.Join(string(d), "new")) 76 if err != nil { 77 return nil, err 78 } 79 defer f.Close() 80 names, err := f.Readdirnames(0) 81 if err != nil { 82 return nil, err 83 } 84 var keys []string 85 for _, n := range names { 86 if n[0] != '.' { 87 split := strings.FieldsFunc(n, func(r rune) bool { 88 return r == separator 89 }) 90 key := split[0] 91 info := "2," 92 // Messages in new shouldn't have an info section but 93 // we act as if, in case some other program didn't 94 // follow the spec. 95 if len(split) > 1 { 96 info = split[1] 97 } 98 keys = append(keys, key) 99 err = os.Rename(filepath.Join(string(d), "new", n), 100 filepath.Join(string(d), "cur", key+string(separator)+info)) 101 } 102 } 103 return keys, err 104} 105 106// UnseenCount returns the number of messages in new without looking at them. 107func (d Dir) UnseenCount() (int, error) { 108 f, err := os.Open(filepath.Join(string(d), "new")) 109 if err != nil { 110 return 0, err 111 } 112 defer f.Close() 113 names, err := f.Readdirnames(0) 114 if err != nil { 115 return 0, err 116 } 117 c := 0 118 for _, n := range names { 119 if n[0] != '.' { 120 c += 1 121 } 122 } 123 return c, nil 124} 125 126// Keys returns a slice of valid keys to access messages by. 127func (d Dir) Keys() ([]string, error) { 128 f, err := os.Open(filepath.Join(string(d), "cur")) 129 if err != nil { 130 return nil, err 131 } 132 defer f.Close() 133 names, err := f.Readdirnames(0) 134 if err != nil { 135 return nil, err 136 } 137 var keys []string 138 for _, n := range names { 139 if n[0] != '.' { 140 split := strings.FieldsFunc(n, func(r rune) bool { 141 return r == separator 142 }) 143 keys = append(keys, split[0]) 144 } 145 } 146 return keys, nil 147} 148 149func (d Dir) filenameGuesses(key string) []string { 150 basename := filepath.Join(string(d), "cur", key+string(separator)+"2,") 151 return []string{ 152 basename, 153 154 basename + string(FlagPassed), 155 basename + string(FlagReplied), 156 basename + string(FlagSeen), 157 basename + string(FlagDraft), 158 basename + string(FlagFlagged), 159 160 basename + string(FlagPassed), 161 basename + string(FlagPassed) + string(FlagSeen), 162 basename + string(FlagPassed) + string(FlagSeen) + string(FlagFlagged), 163 basename + string(FlagPassed) + string(FlagFlagged), 164 165 basename + string(FlagReplied) + string(FlagSeen), 166 basename + string(FlagReplied) + string(FlagSeen) + string(FlagFlagged), 167 basename + string(FlagReplied) + string(FlagFlagged), 168 169 basename + string(FlagSeen) + string(FlagFlagged), 170 } 171} 172 173// Filename returns the path to the file corresponding to the key. 174func (d Dir) Filename(key string) (string, error) { 175 // before doing an expensive Glob, see if we can guess the path based on some 176 // common flags 177 for _, guess := range d.filenameGuesses(key) { 178 if _, err := os.Stat(guess); err == nil { 179 return guess, nil 180 } 181 } 182 matches, err := filepath.Glob(filepath.Join(string(d), "cur", key+"*")) 183 if err != nil { 184 return "", err 185 } 186 if n := len(matches); n != 1 { 187 return "", &KeyError{key, n} 188 } 189 return matches[0], nil 190} 191 192// Open reads a message by key. 193func (d Dir) Open(key string) (io.ReadCloser, error) { 194 filename, err := d.Filename(key) 195 if err != nil { 196 return nil, err 197 } 198 return os.Open(filename) 199} 200 201type Flag rune 202 203const ( 204 // The user has resent/forwarded/bounced this message to someone else. 205 FlagPassed Flag = 'P' 206 // The user has replied to this message. 207 FlagReplied Flag = 'R' 208 // The user has viewed this message, though perhaps he didn't read all the 209 // way through it. 210 FlagSeen Flag = 'S' 211 // The user has moved this message to the trash; the trash will be emptied 212 // by a later user action. 213 FlagTrashed Flag = 'T' 214 // The user considers this message a draft; toggled at user discretion. 215 FlagDraft Flag = 'D' 216 // User-defined flag; toggled at user discretion. 217 FlagFlagged Flag = 'F' 218) 219 220type flagList []Flag 221 222func (s flagList) Len() int { return len(s) } 223func (s flagList) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 224func (s flagList) Less(i, j int) bool { return s[i] < s[j] } 225 226// Flags returns the flags for a message sorted in ascending order. 227// See the documentation of SetFlags for details. 228func (d Dir) Flags(key string) ([]Flag, error) { 229 filename, err := d.Filename(key) 230 if err != nil { 231 return nil, err 232 } 233 split := strings.FieldsFunc(filename, func(r rune) bool { 234 return r == separator 235 }) 236 switch { 237 case len(split) <= 1: 238 return nil, &MailfileError{filename} 239 case len(split[1]) < 2, 240 split[1][1] != ',': 241 return nil, &FlagError{split[1], false} 242 case split[1][0] == '1': 243 return nil, &FlagError{split[1], true} 244 case split[1][0] != '2': 245 return nil, &FlagError{split[1], false} 246 } 247 fl := flagList(split[1][2:]) 248 sort.Sort(fl) 249 return []Flag(fl), nil 250} 251 252func formatInfo(flags []Flag) string { 253 info := "2," 254 fl := flagList(flags) 255 sort.Sort(fl) 256 for _, f := range fl { 257 if []rune(info)[len(info)-1] != rune(f) { 258 info += string(f) 259 } 260 } 261 return info 262} 263 264// SetFlags appends an info section to the filename according to the given flags. 265// This function removes duplicates and sorts the flags, but doesn't check 266// whether they conform with the Maildir specification. 267func (d Dir) SetFlags(key string, flags []Flag) error { 268 return d.SetInfo(key, formatInfo(flags)) 269} 270 271// Set the info part of the filename. 272// Only use this if you plan on using a non-standard info part. 273func (d Dir) SetInfo(key, info string) error { 274 filename, err := d.Filename(key) 275 if err != nil { 276 return err 277 } 278 err = os.Rename(filename, filepath.Join(string(d), "cur", key+ 279 string(separator)+info)) 280 return err 281} 282 283// newKey generates a new unique key as described in the Maildir specification. 284// For the third part of the key (delivery identifier) it uses an internal 285// counter, the process id and a cryptographical random number to ensure 286// uniqueness among messages delivered in the same second. 287func newKey() (string, error) { 288 var key string 289 key += strconv.FormatInt(time.Now().Unix(), 10) 290 key += "." 291 host, err := os.Hostname() 292 if err != err { 293 return "", err 294 } 295 host = strings.Replace(host, "/", "\057", -1) 296 host = strings.Replace(host, string(separator), "\072", -1) 297 key += host 298 key += "." 299 key += strconv.FormatInt(int64(os.Getpid()), 10) 300 key += strconv.FormatInt(id, 10) 301 atomic.AddInt64(&id, 1) 302 bs := make([]byte, 10) 303 _, err = io.ReadFull(rand.Reader, bs) 304 if err != nil { 305 return "", err 306 } 307 key += hex.EncodeToString(bs) 308 return key, nil 309} 310 311// Init creates the directory structure for a Maildir. 312// 313// If the main directory already exists, it tries to create the subdirectories 314// in there. If an error occurs while creating one of the subdirectories, this 315// function may leave a partially created directory structure. 316func (d Dir) Init() error { 317 err := os.Mkdir(string(d), os.ModeDir|createMode) 318 if err != nil && !os.IsExist(err) { 319 return err 320 } 321 err = os.Mkdir(filepath.Join(string(d), "tmp"), os.ModeDir|createMode) 322 if err != nil && !os.IsExist(err) { 323 return err 324 } 325 err = os.Mkdir(filepath.Join(string(d), "new"), os.ModeDir|createMode) 326 if err != nil && !os.IsExist(err) { 327 return err 328 } 329 err = os.Mkdir(filepath.Join(string(d), "cur"), os.ModeDir|createMode) 330 if err != nil && !os.IsExist(err) { 331 return err 332 } 333 return nil 334} 335 336// Move moves a message from this Maildir to another. 337func (d Dir) Move(target Dir, key string) error { 338 path, err := d.Filename(key) 339 if err != nil { 340 return err 341 } 342 return os.Rename(path, filepath.Join(string(target), "cur", filepath.Base(path))) 343} 344 345// Copy copies the message with key from this Maildir to the target, preserving 346// its flags, returning the newly generated key for the target maildir or an 347// error. 348func (d Dir) Copy(target Dir, key string) (string, error) { 349 flags, err := d.Flags(key) 350 if err != nil { 351 return "", err 352 } 353 targetKey, err := d.copyToTmp(target, key) 354 if err != nil { 355 return "", err 356 } 357 tmpfile := filepath.Join(string(target), "tmp", targetKey) 358 curfile := filepath.Join(string(target), "cur", targetKey+"2,") 359 if err = os.Rename(tmpfile, curfile); err != nil { 360 return "", err 361 } 362 if err = target.SetFlags(targetKey, flags); err != nil { 363 return "", err 364 } 365 return targetKey, nil 366} 367 368// copyToTmp copies the message with key from d into a file in the target 369// maildir's tmp directory with a new key, returning the newly generated key or 370// an error. 371func (d Dir) copyToTmp(target Dir, key string) (string, error) { 372 rc, err := d.Open(key) 373 if err != nil { 374 return "", err 375 } 376 defer rc.Close() 377 targetKey, err := newKey() 378 if err != nil { 379 return "", err 380 } 381 tmpfile := filepath.Join(string(target), "tmp", targetKey) 382 wc, err := os.OpenFile(tmpfile, os.O_CREATE|os.O_WRONLY, 0600) 383 if err != nil { 384 return "", err 385 } 386 defer wc.Close() 387 if _, err = io.Copy(wc, rc); err != nil { 388 return "", err 389 } 390 return targetKey, nil 391} 392 393// Create inserts a new message into the Maildir. 394func (d Dir) Create(flags []Flag) (key string, w io.WriteCloser, err error) { 395 key, err = newKey() 396 if err != nil { 397 return "", nil, err 398 } 399 name := key + string(separator) + formatInfo(flags) 400 w, err = os.Create(filepath.Join(string(d), "cur", name)) 401 if err != nil { 402 return "", nil, err 403 } 404 return key, w, err 405} 406 407// Remove removes the actual file behind this message. 408func (d Dir) Remove(key string) error { 409 f, err := d.Filename(key) 410 if err != nil { 411 return err 412 } 413 return os.Remove(f) 414} 415 416// Clean removes old files from tmp and should be run periodically. 417// This does not use access time but modification time for portability reasons. 418func (d Dir) Clean() error { 419 f, err := os.Open(filepath.Join(string(d), "tmp")) 420 if err != nil { 421 return err 422 } 423 defer f.Close() 424 names, err := f.Readdirnames(0) 425 if err != nil { 426 return err 427 } 428 now := time.Now() 429 for _, n := range names { 430 fi, err := os.Stat(filepath.Join(string(d), "tmp", n)) 431 if err != nil { 432 continue 433 } 434 if now.Sub(fi.ModTime()).Hours() > 36 { 435 err = os.Remove(filepath.Join(string(d), "tmp", n)) 436 if err != nil { 437 return err 438 } 439 } 440 } 441 return nil 442} 443 444// Delivery represents an ongoing message delivery to the mailbox. It 445// implements the io.WriteCloser interface. On Close the underlying file is 446// moved/relinked to new. 447// 448// Multiple processes can perform a delivery on the same Maildir concurrently. 449type Delivery struct { 450 file *os.File 451 d Dir 452 key string 453} 454 455// NewDelivery creates a new Delivery. 456func NewDelivery(d string) (*Delivery, error) { 457 key, err := newKey() 458 if err != nil { 459 return nil, err 460 } 461 del := &Delivery{} 462 file, err := os.Create(filepath.Join(d, "tmp", key)) 463 if err != nil { 464 return nil, err 465 } 466 del.file = file 467 del.d = Dir(d) 468 del.key = key 469 return del, nil 470} 471 472// Write implements io.Writer. 473func (d *Delivery) Write(p []byte) (int, error) { 474 return d.file.Write(p) 475} 476 477// Close closes the underlying file and moves it to new. 478func (d *Delivery) Close() error { 479 tmppath := d.file.Name() 480 err := d.file.Close() 481 if err != nil { 482 return err 483 } 484 err = os.Link(tmppath, filepath.Join(string(d.d), "new", d.key)) 485 if err != nil { 486 return err 487 } 488 err = os.Remove(tmppath) 489 if err != nil { 490 return err 491 } 492 return nil 493} 494 495// Abort closes the underlying file and removes it completely. 496func (d *Delivery) Abort() error { 497 tmppath := d.file.Name() 498 err := d.file.Close() 499 if err != nil { 500 return err 501 } 502 err = os.Remove(tmppath) 503 if err != nil { 504 return err 505 } 506 return nil 507} 508