package main import ( "flag" "fmt" "io" "io/ioutil" "log" "mime" "mime/multipart" "net/mail" "os" "path" "path/filepath" "runtime" "time" ) var ( maildirGlob = flag.String("maildir", os.ExpandEnv("${HOME}/Maildir/.*/*/*"), "Pattern for email sent files") attachementsDir = flag.String("attachementsDir", os.ExpandEnv("${HOME}/.attachments"), "Directory to store attachements in") ) type Attachment struct { Part multipart.Part Bytes []byte } type Email struct { FileName string Message *mail.Message Attachments []Attachment } func NewEmail(fn string) (*Email, error) { //log.Print("Parsing ", fn) email := &Email{ FileName: fn, Attachments: []Attachment{}, } err := email.ParseMessage() if err != nil { return nil, err } err = email.ParseParts(email.Message) if err != nil { return nil, err } return email, nil } func (e *Email) String() string { return fmt.Sprintf("%s:\n\n%#v\n\n%#v\n\n%#v", e.FileName, e.Message, e.Attachments) } func (e *Email) ParseMessage() error { r, err := os.Open(e.FileName) if err != nil { return err } e.Message, err = mail.ReadMessage(r) if err != nil { return err } return nil } func (e *Email) ParseSubParts(part *multipart.Part) ([]byte, error) { v := part.Header.Get("Content-Type") _, params, err := mime.ParseMediaType(v) if err != nil { bytes, err := ioutil.ReadAll(part) return bytes, err } boundary, ok := params["boundary"] if !ok { // Not multipart bytes, err := ioutil.ReadAll(part) return bytes, err } pr := multipart.NewReader(part, boundary) for { p, err := pr.NextPart() switch err { case nil: bytes, err := e.ParseSubParts(p) if err != nil { return nil, err } if bytes != nil { e.Attachments = append(e.Attachments, Attachment{ Part: *p, Bytes: bytes, }) } case io.EOF: return nil, nil default: return nil, err } } panic("never reached") } func (e *Email) ParseParts(msg *mail.Message) error { v := msg.Header.Get("Content-Type") _, params, err := mime.ParseMediaType(v) if err != nil { // Not multipart return nil } boundary, ok := params["boundary"] if !ok { // Not multipart return nil } pr := multipart.NewReader(e.Message.Body, boundary) for { part, err := pr.NextPart() switch err { case nil: bytes, err := e.ParseSubParts(part) if err != nil { return err } if bytes != nil { e.Attachments = append(e.Attachments, Attachment{ Part: *part, Bytes: bytes, }) } case io.EOF: return nil default: return err } } panic("never reached") } func createDirIfNeeded(dir string) { _, err := os.Stat(dir) if err != nil { log.Println("Creating", dir) os.MkdirAll(dir, 0700) } } func (e *Email) saveAttachment(dir string, a *Attachment) error { fn := a.Part.FileName() if fn != "" { createDirIfNeeded(dir) targetFn := path.Join(dir, fn) log.Println("Saving", targetFn) f, err := os.OpenFile(targetFn, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { return err } defer f.Close() n, err := f.Write(a.Bytes) log.Printf("savePart copied %d bytes", n) if err != nil { return err } } return nil } func (e *Email) SaveAttachments(dir string) error { for _, a := range e.Attachments { if err := e.saveAttachment(dir, &a); err != nil { return err } } return nil } func main() { flag.Parse() ncpus := runtime.NumCPU() runtime.GOMAXPROCS(ncpus) log.Printf("Parsing %s with %d workers", *maildirGlob, ncpus) files, err := filepath.Glob(*maildirGlob) if err != nil { log.Fatalf("Failed to glob %q: %s", *maildirGlob, err) } worker := func(fnCh chan string, done chan bool) { for fn := range fnCh { email, err := NewEmail(fn) if err != nil { log.Printf("Error parsing %q: %s", fn, err) continue } d, err := email.Message.Header.Date() if err != nil { log.Printf("Error parsing date in %q: %s", fn, err) continue } subdir := fmt.Sprintf("%04d/%02d/%02d", d.Year(), d.Month(), d.Day()) dir := path.Join(*attachementsDir, subdir) email.SaveAttachments(dir) } done <- true } ch := make(chan string, ncpus) done := make(chan bool, 1) for i := 0; i < ncpus; i++ { go worker(ch, done) } start := time.Now() reportChunk := 1000 for idx, fn := range files { if idx != 0 && idx%reportChunk == 0 { delta := time.Since(start) log.Printf("Processing %d/%d %.2f msg/s", idx, len(files), float64(reportChunk)/delta.Seconds()) start = time.Now() } ch <- fn } close(ch) for i := 0; i < ncpus; i++ { <-done } } func NewMimeReader(msg *mail.Message) *multipart.Reader { v := msg.Header.Get("Content-Type") _, params, err := mime.ParseMediaType(v) if err != nil { return nil } boundary, ok := params["boundary"] if !ok { //log.Printf("Found Content-Type %q, missing boundary", d) return nil } return multipart.NewReader(msg.Body, boundary) } func PrintMimeParts(r *multipart.Reader) { for { part, err := r.NextPart() if err == io.EOF { return } if err != nil { log.Print("Failed to parse mime part ", err) return } //log.Print("Header ", part.Header) content, ok := part.Header["Content-Disposition"] if ok { log.Print("\tContent-Disposition ", content) if part.FileName() != "" { log.Print("FileName ", part.FileName()) } if part.FormName() != "" { log.Print("FormName ", part.FormName()) } } } }