package smtpd import ( "bufio" "encoding/gob" "errors" "fmt" "github.com/jhillyerd/go.enmime" "github.com/jhillyerd/inbucket/config" "github.com/jhillyerd/inbucket/log" "io" "io/ioutil" "net/mail" "os" "path/filepath" "strings" "time" ) var ErrNotWritable = errors.New("Message not writable") // Global because we only want one regardless of the number of DataStore objects var countChannel = make(chan int, 10) func init() { // Start generator go countGenerator(countChannel) } // Populates the channel with numbers func countGenerator(c chan int) { for i := 0; true; i = (i + 1) % 10000 { c <- i } } // A DataStore is the root of the mail storage hiearchy. It provides access to // Mailbox objects type FileDataStore struct { path string mailPath string } // NewDataStore creates a new DataStore object. It uses the inbucket.Config object to // construct it's path. func NewFileDataStore() DataStore { path, err := config.Config.String("datastore", "path") if err != nil { log.LogError("Error getting datastore path: %v", err) return nil } if path == "" { log.LogError("No value configured for datastore path") return nil } mailPath := filepath.Join(path, "mail") return &FileDataStore{path: path, mailPath: mailPath} } // Retrieves the Mailbox object for a specified email address, if the mailbox // does not exist, it will attempt to create it. func (ds *FileDataStore) MailboxFor(emailAddress string) (Mailbox, error) { name := ParseMailboxName(emailAddress) dir := HashMailboxName(name) s1 := dir[0:3] s2 := dir[0:6] path := filepath.Join(ds.mailPath, s1, s2, dir) if err := os.MkdirAll(path, 0770); err != nil { log.LogError("Failed to create directory %v, %v", path, err) return nil, err } return &FileMailbox{store: ds, name: name, dirName: dir, path: path}, nil } // AllMailboxes returns a slice with all Mailboxes func (ds *FileDataStore) AllMailboxes() ([]Mailbox, error) { mailboxes := make([]Mailbox, 0, 100) infos1, err := ioutil.ReadDir(ds.mailPath) if err != nil { return nil, err } // Loop over level 1 directories for _, inf1 := range infos1 { if inf1.IsDir() { l1 := inf1.Name() infos2, err := ioutil.ReadDir(filepath.Join(ds.mailPath, l1)) if err != nil { return nil, err } // Loop over level 2 directories for _, inf2 := range infos2 { if inf2.IsDir() { l2 := inf2.Name() infos3, err := ioutil.ReadDir(filepath.Join(ds.mailPath, l1, l2)) if err != nil { return nil, err } // Loop over mailboxes for _, inf3 := range infos3 { if inf3.IsDir() { mbdir := inf3.Name() mbpath := filepath.Join(ds.mailPath, l1, l2, mbdir) mb := &FileMailbox{store: ds, dirName: mbdir, path: mbpath} mailboxes = append(mailboxes, mb) } } } } } } return mailboxes, nil } // A Mailbox manages the mail for a specific user and correlates to a particular // directory on disk. type FileMailbox struct { store *FileDataStore name string dirName string path string } func (mb *FileMailbox) String() string { return mb.name + "[" + mb.dirName + "]" } // GetMessages scans the mailbox directory for .gob files and decodes them into // a slice of Message objects. func (mb *FileMailbox) GetMessages() ([]Message, error) { files, err := ioutil.ReadDir(mb.path) if err != nil { return nil, err } log.LogTrace("Scanning %v files for %v", len(files), mb) messages := make([]Message, 0, len(files)) for _, f := range files { if (!f.IsDir()) && strings.HasSuffix(strings.ToLower(f.Name()), ".gob") { // We have a gob file file, err := os.Open(filepath.Join(mb.path, f.Name())) if err != nil { return nil, err } dec := gob.NewDecoder(bufio.NewReader(file)) msg := new(FileMessage) if err = dec.Decode(msg); err != nil { return nil, fmt.Errorf("While decoding message: %v", err) } file.Close() msg.mailbox = mb log.LogTrace("Found: %v", msg) messages = append(messages, msg) } } return messages, nil } // GetMessage decodes a single message by Id and returns a Message object func (mb *FileMailbox) GetMessage(id string) (Message, error) { file, err := os.Open(filepath.Join(mb.path, id+".gob")) if err != nil { return nil, err } dec := gob.NewDecoder(bufio.NewReader(file)) msg := new(FileMessage) if err = dec.Decode(msg); err != nil { return nil, err } file.Close() msg.mailbox = mb log.LogTrace("Found: %v", msg) return msg, nil } // Message contains a little bit of data about a particular email message, and // methods to retrieve the rest of it from disk. type FileMessage struct { mailbox *FileMailbox // Stored in GOB Fid string Fdate time.Time Ffrom string Fsubject string // These are for creating new messages only writable bool writerFile *os.File writer *bufio.Writer } // NewMessage creates a new Message object and sets the Date and Id fields. func (mb *FileMailbox) NewMessage() Message { date := time.Now() id := generateId(date) return &FileMessage{mailbox: mb, Fid: id, Fdate: date, writable: true} } func (m *FileMessage) Id() string { return m.Fid } func (m *FileMessage) Date() time.Time { return m.Fdate } func (m *FileMessage) From() string { return m.Ffrom } func (m *FileMessage) Subject() string { return m.Fsubject } func (m *FileMessage) String() string { return fmt.Sprintf("\"%v\" from %v", m.Fsubject, m.Ffrom) } func (m *FileMessage) Size() int64 { fi, err := os.Stat(m.rawPath()) if err != nil { return 0 } return fi.Size() } func (m *FileMessage) gobPath() string { return filepath.Join(m.mailbox.path, m.Fid+".gob") } func (m *FileMessage) rawPath() string { return filepath.Join(m.mailbox.path, m.Fid+".raw") } // ReadHeader opens the .raw portion of a Message and returns a standard Go mail.Message object func (m *FileMessage) ReadHeader() (msg *mail.Message, err error) { file, err := os.Open(m.rawPath()) defer file.Close() if err != nil { return nil, err } reader := bufio.NewReader(file) msg, err = mail.ReadMessage(reader) return msg, err } // ReadBody opens the .raw portion of a Message and returns a MIMEBody object, along // with a free mail.Message containing the Headers, since we had to make one of those // anyway. func (m *FileMessage) ReadBody() (msg *mail.Message, body *enmime.MIMEBody, err error) { file, err := os.Open(m.rawPath()) defer file.Close() if err != nil { return nil, nil, err } reader := bufio.NewReader(file) msg, err = mail.ReadMessage(reader) if err != nil { return nil, nil, err } mime, err := enmime.ParseMIMEBody(msg) if err != nil { return nil, nil, err } return msg, mime, err } // RawReader opens the .raw portion of a Message as an io.ReadCloser func (m *FileMessage) RawReader() (reader io.ReadCloser, err error) { file, err := os.Open(m.rawPath()) if err != nil { return nil, err } return file, nil } // ReadRaw opens the .raw portion of a Message and returns it as a string func (m *FileMessage) ReadRaw() (raw *string, err error) { reader, err := m.RawReader() defer reader.Close() if err != nil { return nil, err } bodyBytes, err := ioutil.ReadAll(bufio.NewReader(reader)) if err != nil { return nil, err } bodyString := string(bodyBytes) return &bodyString, nil } // Append data to a newly opened Message, this will fail on a pre-existing Message and // after Close() is called. func (m *FileMessage) Append(data []byte) error { // Prevent Appending to a pre-existing Message if !m.writable { return ErrNotWritable } // Open file for writing if we haven't yet if m.writer == nil { file, err := os.Create(m.rawPath()) if err != nil { // Set writable false just in case something calls me a million times m.writable = false return err } m.writerFile = file m.writer = bufio.NewWriter(file) } _, err := m.writer.Write(data) return err } // Close this Message for writing - no more data may be Appended. Close() will also // trigger the creation of the .gob file. func (m *FileMessage) Close() error { // nil out the writer fields so they can't be used writer := m.writer writerFile := m.writerFile m.writer = nil m.writerFile = nil if writer != nil { if err := writer.Flush(); err != nil { return err } } if writerFile != nil { if err := writerFile.Close(); err != nil { return err } } err := m.createGob() if err != nil { log.LogError("Failed to create gob: %v", err) return err } return nil } // Delete this Message from disk by removing both the gob and raw files func (m *FileMessage) Delete() error { log.LogTrace("Deleting %v", m.gobPath()) err := os.Remove(m.gobPath()) if err != nil { return err } log.LogTrace("Deleting %v", m.rawPath()) return os.Remove(m.rawPath()) } // createGob reads the .raw file to grab the From and Subject header entries, // then creates the .gob file. func (m *FileMessage) createGob() error { // Open gob for writing file, err := os.Create(m.gobPath()) defer file.Close() if err != nil { return err } writer := bufio.NewWriter(file) // Fetch headers msg, err := m.ReadHeader() if err != nil { return err } // Only public fields are stored in gob m.Ffrom = msg.Header.Get("From") m.Fsubject = msg.Header.Get("Subject") // Write & flush enc := gob.NewEncoder(writer) err = enc.Encode(m) if err != nil { return err } writer.Flush() return nil } // generatePrefix converts a Time object into the ISO style format we use // as a prefix for message files. Note: It is used directly by unit // tests. func generatePrefix(date time.Time) string { return date.Format("20060102T150405") } // generateId adds a 4-digit unique number onto the end of the string // returned by generatePrefix() func generateId(date time.Time) string { return generatePrefix(date) + "-" + fmt.Sprintf("%04d", <-countChannel) }