mirror of https://github.com/gophish/gophish
207 lines
5.0 KiB
Go
207 lines
5.0 KiB
Go
package imap
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/emersion/go-imap"
|
|
"github.com/emersion/go-imap/client"
|
|
"github.com/emersion/go-message/charset"
|
|
log "github.com/gophish/gophish/logger"
|
|
"github.com/gophish/gophish/models"
|
|
|
|
"github.com/jordan-wright/email"
|
|
)
|
|
|
|
// Client interface for IMAP interactions
|
|
type Client interface {
|
|
Login(username, password string) (cmd *imap.Command, err error)
|
|
Logout(timeout time.Duration) (cmd *imap.Command, err error)
|
|
Select(name string, readOnly bool) (mbox *imap.MailboxStatus, err error)
|
|
Store(seq *imap.SeqSet, item imap.StoreItem, value interface{}, ch chan *imap.Message) (err error)
|
|
Fetch(seqset *imap.SeqSet, items []imap.FetchItem, ch chan *imap.Message) (err error)
|
|
}
|
|
|
|
// Email represents an email.Email with an included IMAP Sequence Number
|
|
type Email struct {
|
|
SeqNum uint32 `json:"seqnum"`
|
|
*email.Email
|
|
}
|
|
|
|
// Mailbox holds onto the credentials and other information
|
|
// needed for connecting to an IMAP server.
|
|
type Mailbox struct {
|
|
Host string
|
|
TLS bool
|
|
User string
|
|
Pwd string
|
|
Folder string
|
|
// Read only mode, false (original logic) if not initialized
|
|
ReadOnly bool
|
|
}
|
|
|
|
// Validate validates supplied IMAP model by connecting to the server
|
|
func Validate(s *models.IMAP) error {
|
|
err := s.Validate()
|
|
if err != nil {
|
|
log.Error(err)
|
|
return err
|
|
}
|
|
|
|
s.Host = s.Host + ":" + strconv.Itoa(int(s.Port)) // Append port
|
|
mailServer := Mailbox{
|
|
Host: s.Host,
|
|
TLS: s.TLS,
|
|
User: s.Username,
|
|
Pwd: s.Password,
|
|
Folder: s.Folder}
|
|
|
|
imapClient, err := mailServer.newClient()
|
|
if err != nil {
|
|
log.Error(err.Error())
|
|
} else {
|
|
imapClient.Logout()
|
|
}
|
|
return err
|
|
}
|
|
|
|
// MarkAsUnread will set the UNSEEN flag on a supplied slice of SeqNums
|
|
func (mbox *Mailbox) MarkAsUnread(seqs []uint32) error {
|
|
imapClient, err := mbox.newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer imapClient.Logout()
|
|
|
|
seqSet := new(imap.SeqSet)
|
|
seqSet.AddNum(seqs...)
|
|
|
|
item := imap.FormatFlagsOp(imap.RemoveFlags, true)
|
|
err = imapClient.Store(seqSet, item, imap.SeenFlag, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
// DeleteEmails will delete emails from the supplied slice of SeqNums
|
|
func (mbox *Mailbox) DeleteEmails(seqs []uint32) error {
|
|
imapClient, err := mbox.newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer imapClient.Logout()
|
|
|
|
seqSet := new(imap.SeqSet)
|
|
seqSet.AddNum(seqs...)
|
|
|
|
item := imap.FormatFlagsOp(imap.AddFlags, true)
|
|
err = imapClient.Store(seqSet, item, imap.DeletedFlag, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetUnread will find all unread emails in the folder and return them as a list.
|
|
func (mbox *Mailbox) GetUnread(markAsRead, delete bool) ([]Email, error) {
|
|
imap.CharsetReader = charset.Reader
|
|
var emails []Email
|
|
|
|
imapClient, err := mbox.newClient()
|
|
if err != nil {
|
|
return emails, fmt.Errorf("failed to create IMAP connection: %s", err)
|
|
}
|
|
|
|
defer imapClient.Logout()
|
|
|
|
// Search for unread emails
|
|
criteria := imap.NewSearchCriteria()
|
|
criteria.WithoutFlags = []string{imap.SeenFlag}
|
|
seqs, err := imapClient.Search(criteria)
|
|
if err != nil {
|
|
return emails, err
|
|
}
|
|
|
|
if len(seqs) == 0 {
|
|
return emails, nil
|
|
}
|
|
|
|
seqset := new(imap.SeqSet)
|
|
seqset.AddNum(seqs...)
|
|
section := &imap.BodySectionName{}
|
|
items := []imap.FetchItem{imap.FetchEnvelope, imap.FetchFlags, imap.FetchInternalDate, section.FetchItem()}
|
|
messages := make(chan *imap.Message)
|
|
|
|
go func() {
|
|
if err := imapClient.Fetch(seqset, items, messages); err != nil {
|
|
log.Error("Error fetching emails: ", err.Error()) // TODO: How to handle this, need to propogate error out
|
|
}
|
|
}()
|
|
|
|
// Step through each email
|
|
for msg := range messages {
|
|
// Extract raw message body. I can't find a better way to do this with the emersion library
|
|
var em *email.Email
|
|
var buf []byte
|
|
for _, value := range msg.Body {
|
|
buf = make([]byte, value.Len())
|
|
value.Read(buf)
|
|
break // There should only ever be one item in this map, but I'm not 100% sure
|
|
}
|
|
|
|
//Remove CR characters, see https://github.com/jordan-wright/email/issues/106
|
|
tmp := string(buf)
|
|
re := regexp.MustCompile(`\r`)
|
|
tmp = re.ReplaceAllString(tmp, "")
|
|
buf = []byte(tmp)
|
|
|
|
rawBodyStream := bytes.NewReader(buf)
|
|
em, err = email.NewEmailFromReader(rawBodyStream) // Parse with @jordanwright's library
|
|
if err != nil {
|
|
return emails, err
|
|
}
|
|
|
|
emtmp := Email{Email: em, SeqNum: msg.SeqNum} // Not sure why msg.Uid is always 0, so swapped to sequence numbers
|
|
emails = append(emails, emtmp)
|
|
|
|
}
|
|
return emails, nil
|
|
}
|
|
|
|
// newClient will initiate a new IMAP connection with the given creds.
|
|
func (mbox *Mailbox) newClient() (*client.Client, error) {
|
|
var imapClient *client.Client
|
|
var err error
|
|
if mbox.TLS {
|
|
imapClient, err = client.DialTLS(mbox.Host, new(tls.Config))
|
|
} else {
|
|
imapClient, err = client.Dial(mbox.Host)
|
|
}
|
|
if err != nil {
|
|
return imapClient, err
|
|
}
|
|
|
|
|
|
err = imapClient.Login(mbox.User, mbox.Pwd)
|
|
if err != nil {
|
|
return imapClient, err
|
|
}
|
|
|
|
_, err = imapClient.Select(mbox.Folder, mbox.ReadOnly)
|
|
if err != nil {
|
|
return imapClient, err
|
|
}
|
|
|
|
return imapClient, nil
|
|
}
|