mirror of https://github.com/gophish/gophish
352 lines
8.5 KiB
Go
352 lines
8.5 KiB
Go
package imap
|
|
|
|
// Functionality taken from https://github.com/jprobinson/eazye
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"strconv"
|
|
"time"
|
|
|
|
log "github.com/gophish/gophish/logger"
|
|
"github.com/gophish/gophish/models"
|
|
"github.com/jordan-wright/email"
|
|
"github.com/mxk/go-imap/imap"
|
|
)
|
|
|
|
// Client interface for IMAP interactions
|
|
type Client interface {
|
|
Close(expunge bool) (cmd *imap.Command, err error)
|
|
Login(username, password string) (cmd *imap.Command, err error)
|
|
Logout(timeout time.Duration) (cmd *imap.Command, err error)
|
|
Select(mbox string, readonly bool) (cmd *imap.Command, err error)
|
|
UIDFetch(seq *imap.SeqSet, items ...string) (cmd *imap.Command, err error)
|
|
UIDSearch(spec ...imap.Field) (cmd *imap.Command, err error)
|
|
UIDStore(seq *imap.SeqSet, item string, value imap.Field) (cmd *imap.Command, err error)
|
|
}
|
|
|
|
// Email represents an email.Email with an included IMAP UID
|
|
type Email struct {
|
|
UID uint32 `json:"uid"`
|
|
*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
|
|
}
|
|
|
|
// GetAll will pull all emails from the email folder and return them as a list.
|
|
func (mbox *Mailbox) GetAll(markAsRead, delete bool) ([]Email, error) {
|
|
// call chan, put 'em in a list, return
|
|
var emails []Email
|
|
responses, err := mbox.GenerateAll(markAsRead, delete)
|
|
if err != nil {
|
|
return emails, err
|
|
}
|
|
|
|
for resp := range responses {
|
|
if resp.Err != nil {
|
|
return emails, resp.Err
|
|
}
|
|
emails = append(emails, resp.Email)
|
|
}
|
|
|
|
return emails, nil
|
|
}
|
|
|
|
// GenerateAll will find all emails in the email folder and pass them along to the responses channel.
|
|
func (mbox *Mailbox) GenerateAll(markAsRead, delete bool) (chan Response, error) {
|
|
return mbox.generateMail("ALL", nil, markAsRead, delete)
|
|
}
|
|
|
|
// GetUnread will find all unread emails in the folder and return them as a list.
|
|
func (mbox *Mailbox) GetUnread(markAsRead, delete bool) ([]Email, error) {
|
|
// call chan, put 'em in a list, return
|
|
var emails []Email
|
|
|
|
responses, err := mbox.GenerateUnread(markAsRead, delete)
|
|
if err != nil {
|
|
return emails, err
|
|
}
|
|
|
|
for resp := range responses {
|
|
if resp.Err != nil {
|
|
return emails, resp.Err
|
|
}
|
|
emails = append(emails, resp.Email)
|
|
}
|
|
|
|
return emails, nil
|
|
}
|
|
|
|
// GenerateUnread will find all unread emails in the folder and pass them along to the responses channel.
|
|
func (mbox *Mailbox) GenerateUnread(markAsRead, delete bool) (chan Response, error) {
|
|
return mbox.generateMail("UNSEEN", nil, markAsRead, delete)
|
|
}
|
|
|
|
// MarkAsUnread will set the UNSEEN flag on a supplied slice of UIDs
|
|
func (mbox *Mailbox) MarkAsUnread(uids []uint32) error {
|
|
client, err := mbox.newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
client.Close(true)
|
|
client.Logout(30 * time.Second)
|
|
}()
|
|
for _, u := range uids {
|
|
err := alterEmail(client, u, "\\SEEN", false)
|
|
if err != nil {
|
|
return err //return on first failure
|
|
}
|
|
}
|
|
return nil
|
|
|
|
}
|
|
|
|
// DeleteEmails will delete emails from the supplied slice of UIDs
|
|
func (mbox *Mailbox) DeleteEmails(uids []uint32) error {
|
|
client, err := mbox.newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
client.Close(true)
|
|
client.Logout(30 * time.Second)
|
|
}()
|
|
for _, u := range uids {
|
|
err := deleteEmail(client, u)
|
|
if err != nil {
|
|
return err //return on first failure
|
|
}
|
|
}
|
|
return nil
|
|
|
|
}
|
|
|
|
// 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}
|
|
|
|
client, err := mailServer.newClient()
|
|
if err != nil {
|
|
log.Error(err.Error())
|
|
} else {
|
|
client.Close(true)
|
|
client.Logout(30 * time.Second)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Response is a helper struct to wrap the email responses and possible errors.
|
|
type Response struct {
|
|
Email Email
|
|
Err error
|
|
}
|
|
|
|
// newClient will initiate a new IMAP connection with the given creds.
|
|
func (mbox *Mailbox) newClient() (*imap.Client, error) {
|
|
var client *imap.Client
|
|
var err error
|
|
if mbox.TLS {
|
|
client, err = imap.DialTLS(mbox.Host, new(tls.Config))
|
|
if err != nil {
|
|
return client, err
|
|
}
|
|
} else {
|
|
client, err = imap.Dial(mbox.Host)
|
|
if err != nil {
|
|
return client, err
|
|
}
|
|
}
|
|
|
|
_, err = client.Login(mbox.User, mbox.Pwd)
|
|
if err != nil {
|
|
return client, err
|
|
}
|
|
|
|
_, err = imap.Wait(client.Select(mbox.Folder, mbox.ReadOnly))
|
|
if err != nil {
|
|
return client, err
|
|
}
|
|
|
|
return client, nil
|
|
}
|
|
|
|
const dateFormat = "02-Jan-2006"
|
|
|
|
// findEmails will run a find the UIDs of any emails that match the search.:
|
|
func findEmails(client Client, search string, since *time.Time) (*imap.Command, error) {
|
|
var specs []imap.Field
|
|
if len(search) > 0 {
|
|
specs = append(specs, search)
|
|
}
|
|
|
|
if since != nil {
|
|
sinceStr := since.Format(dateFormat)
|
|
specs = append(specs, "SINCE", sinceStr)
|
|
}
|
|
|
|
// get headers and UID for UnSeen message in src inbox...
|
|
cmd, err := imap.Wait(client.UIDSearch(specs...))
|
|
if err != nil {
|
|
return &imap.Command{}, fmt.Errorf("uid search failed: %s", err)
|
|
}
|
|
return cmd, nil
|
|
}
|
|
|
|
const GenerateBufferSize = 100
|
|
|
|
func (mbox *Mailbox) generateMail(search string, since *time.Time, markAsRead, delete bool) (chan Response, error) {
|
|
responses := make(chan Response, GenerateBufferSize)
|
|
client, err := mbox.newClient()
|
|
if err != nil {
|
|
close(responses)
|
|
return responses, fmt.Errorf("failed to create IMAP connection: %s", err)
|
|
}
|
|
|
|
go func() {
|
|
defer func() {
|
|
client.Close(true)
|
|
client.Logout(30 * time.Second)
|
|
close(responses)
|
|
}()
|
|
|
|
var cmd *imap.Command
|
|
// find all the UIDs
|
|
cmd, err = findEmails(client, search, since)
|
|
if err != nil {
|
|
responses <- Response{Err: err}
|
|
return
|
|
}
|
|
// gotta fetch 'em all
|
|
getEmails(client, cmd, markAsRead, delete, responses)
|
|
}()
|
|
|
|
return responses, nil
|
|
}
|
|
|
|
func getEmails(client Client, cmd *imap.Command, markAsRead, delete bool, responses chan Response) {
|
|
seq := &imap.SeqSet{}
|
|
msgCount := 0
|
|
for _, rsp := range cmd.Data {
|
|
for _, uid := range rsp.SearchResults() {
|
|
msgCount++
|
|
seq.AddNum(uid)
|
|
}
|
|
}
|
|
|
|
if seq.Empty() {
|
|
return
|
|
}
|
|
|
|
fCmd, err := imap.Wait(client.UIDFetch(seq, "INTERNALDATE", "BODY[]", "UID", "RFC822.HEADER"))
|
|
if err != nil {
|
|
responses <- Response{Err: fmt.Errorf("unable to perform uid fetch: %s", err)}
|
|
return
|
|
}
|
|
|
|
var email Email
|
|
for _, msgData := range fCmd.Data {
|
|
msgFields := msgData.MessageInfo().Attrs
|
|
|
|
// make sure is a legit response before we attempt to parse it
|
|
// deal with unsolicited FETCH responses containing only flags
|
|
// I'm lookin' at YOU, Gmail!
|
|
// http://mailman13.u.washington.edu/pipermail/imap-protocol/2014-October/002355.html
|
|
// http://stackoverflow.com/questions/26262472/gmail-imap-is-sometimes-returning-bad-results-for-fetch
|
|
if _, ok := msgFields["RFC822.HEADER"]; !ok {
|
|
continue
|
|
}
|
|
|
|
email, err = NewEmail(msgFields)
|
|
if err != nil {
|
|
responses <- Response{Err: fmt.Errorf("unable to parse email: %s", err)}
|
|
return
|
|
}
|
|
|
|
responses <- Response{Email: email}
|
|
|
|
if !markAsRead {
|
|
err = removeSeen(client, imap.AsNumber(msgFields["UID"]))
|
|
if err != nil {
|
|
responses <- Response{Err: fmt.Errorf("unable to remove seen flag: %s", err)}
|
|
return
|
|
}
|
|
}
|
|
|
|
if delete {
|
|
err = deleteEmail(client, imap.AsNumber(msgFields["UID"]))
|
|
if err != nil {
|
|
responses <- Response{Err: fmt.Errorf("unable to delete email: %s", err)}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func deleteEmail(client Client, UID uint32) error {
|
|
return alterEmail(client, UID, "\\DELETED", true)
|
|
}
|
|
|
|
func removeSeen(client Client, UID uint32) error {
|
|
return alterEmail(client, UID, "\\SEEN", false)
|
|
}
|
|
|
|
func alterEmail(client Client, UID uint32, flag string, plus bool) error {
|
|
flg := "-FLAGS"
|
|
if plus {
|
|
flg = "+FLAGS"
|
|
}
|
|
fSeq := &imap.SeqSet{}
|
|
fSeq.AddNum(UID)
|
|
_, err := imap.Wait(client.UIDStore(fSeq, flg, flag))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// NewEmail will parse an imap.FieldMap into an Email. This
|
|
// will expect the message to container the internaldate and the body with
|
|
// all headers included.
|
|
func NewEmail(msgFields imap.FieldMap) (Email, error) {
|
|
|
|
rawBody := imap.AsBytes(msgFields["BODY[]"])
|
|
|
|
rawBodyStream := bytes.NewReader(rawBody)
|
|
em, err := email.NewEmailFromReader(rawBodyStream) // Parse with @jordanwright's library
|
|
if err != nil {
|
|
return Email{}, err
|
|
}
|
|
iem := Email{
|
|
Email: em,
|
|
UID: imap.AsNumber(msgFields["UID"]),
|
|
}
|
|
|
|
return iem, err
|
|
}
|