github.com/gophish/gophish@v0.12.2-0.20230915144530-8e7929441393/imap/imap.go (about)

     1  package imap
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/tls"
     6  	"fmt"
     7  	"regexp"
     8  	"strconv"
     9  	"time"
    10  
    11  	"github.com/emersion/go-imap"
    12  	"github.com/emersion/go-imap/client"
    13  	"github.com/emersion/go-message/charset"
    14  	"github.com/gophish/gophish/dialer"
    15  	log "github.com/gophish/gophish/logger"
    16  	"github.com/gophish/gophish/models"
    17  
    18  	"github.com/jordan-wright/email"
    19  )
    20  
    21  // Client interface for IMAP interactions
    22  type Client interface {
    23  	Login(username, password string) (cmd *imap.Command, err error)
    24  	Logout(timeout time.Duration) (cmd *imap.Command, err error)
    25  	Select(name string, readOnly bool) (mbox *imap.MailboxStatus, err error)
    26  	Store(seq *imap.SeqSet, item imap.StoreItem, value interface{}, ch chan *imap.Message) (err error)
    27  	Fetch(seqset *imap.SeqSet, items []imap.FetchItem, ch chan *imap.Message) (err error)
    28  }
    29  
    30  // Email represents an email.Email with an included IMAP Sequence Number
    31  type Email struct {
    32  	SeqNum uint32 `json:"seqnum"`
    33  	*email.Email
    34  }
    35  
    36  // Mailbox holds onto the credentials and other information
    37  // needed for connecting to an IMAP server.
    38  type Mailbox struct {
    39  	Host             string
    40  	TLS              bool
    41  	IgnoreCertErrors bool
    42  	User             string
    43  	Pwd              string
    44  	Folder           string
    45  	// Read only mode, false (original logic) if not initialized
    46  	ReadOnly bool
    47  }
    48  
    49  // Validate validates supplied IMAP model by connecting to the server
    50  func Validate(s *models.IMAP) error {
    51  	err := s.Validate()
    52  	if err != nil {
    53  		log.Error(err)
    54  		return err
    55  	}
    56  
    57  	s.Host = s.Host + ":" + strconv.Itoa(int(s.Port)) // Append port
    58  	mailServer := Mailbox{
    59  		Host:             s.Host,
    60  		TLS:              s.TLS,
    61  		IgnoreCertErrors: s.IgnoreCertErrors,
    62  		User:             s.Username,
    63  		Pwd:              s.Password,
    64  		Folder:           s.Folder}
    65  
    66  	imapClient, err := mailServer.newClient()
    67  	if err != nil {
    68  		log.Error(err.Error())
    69  	} else {
    70  		imapClient.Logout()
    71  	}
    72  	return err
    73  }
    74  
    75  // MarkAsUnread will set the UNSEEN flag on a supplied slice of SeqNums
    76  func (mbox *Mailbox) MarkAsUnread(seqs []uint32) error {
    77  	imapClient, err := mbox.newClient()
    78  	if err != nil {
    79  		return err
    80  	}
    81  
    82  	defer imapClient.Logout()
    83  
    84  	seqSet := new(imap.SeqSet)
    85  	seqSet.AddNum(seqs...)
    86  
    87  	item := imap.FormatFlagsOp(imap.RemoveFlags, true)
    88  	err = imapClient.Store(seqSet, item, imap.SeenFlag, nil)
    89  	if err != nil {
    90  		return err
    91  	}
    92  
    93  	return nil
    94  
    95  }
    96  
    97  // DeleteEmails will delete emails from the supplied slice of SeqNums
    98  func (mbox *Mailbox) DeleteEmails(seqs []uint32) error {
    99  	imapClient, err := mbox.newClient()
   100  	if err != nil {
   101  		return err
   102  	}
   103  
   104  	defer imapClient.Logout()
   105  
   106  	seqSet := new(imap.SeqSet)
   107  	seqSet.AddNum(seqs...)
   108  
   109  	item := imap.FormatFlagsOp(imap.AddFlags, true)
   110  	err = imapClient.Store(seqSet, item, imap.DeletedFlag, nil)
   111  	if err != nil {
   112  		return err
   113  	}
   114  
   115  	return nil
   116  }
   117  
   118  // GetUnread will find all unread emails in the folder and return them as a list.
   119  func (mbox *Mailbox) GetUnread(markAsRead, delete bool) ([]Email, error) {
   120  	imap.CharsetReader = charset.Reader
   121  	var emails []Email
   122  
   123  	imapClient, err := mbox.newClient()
   124  	if err != nil {
   125  		return emails, fmt.Errorf("failed to create IMAP connection: %s", err)
   126  	}
   127  
   128  	defer imapClient.Logout()
   129  
   130  	// Search for unread emails
   131  	criteria := imap.NewSearchCriteria()
   132  	criteria.WithoutFlags = []string{imap.SeenFlag}
   133  	seqs, err := imapClient.Search(criteria)
   134  	if err != nil {
   135  		return emails, err
   136  	}
   137  
   138  	if len(seqs) == 0 {
   139  		return emails, nil
   140  	}
   141  
   142  	seqset := new(imap.SeqSet)
   143  	seqset.AddNum(seqs...)
   144  	section := &imap.BodySectionName{}
   145  	items := []imap.FetchItem{imap.FetchEnvelope, imap.FetchFlags, imap.FetchInternalDate, section.FetchItem()}
   146  	messages := make(chan *imap.Message)
   147  
   148  	go func() {
   149  		if err := imapClient.Fetch(seqset, items, messages); err != nil {
   150  			log.Error("Error fetching emails: ", err.Error()) // TODO: How to handle this, need to propogate error out
   151  		}
   152  	}()
   153  
   154  	// Step through each email
   155  	for msg := range messages {
   156  		// Extract raw message body. I can't find a better way to do this with the emersion library
   157  		var em *email.Email
   158  		var buf []byte
   159  		for _, value := range msg.Body {
   160  			buf = make([]byte, value.Len())
   161  			value.Read(buf)
   162  			break // There should only ever be one item in this map, but I'm not 100% sure
   163  		}
   164  
   165  		//Remove CR characters, see https://github.com/jordan-wright/email/issues/106
   166  		tmp := string(buf)
   167  		re := regexp.MustCompile(`\r`)
   168  		tmp = re.ReplaceAllString(tmp, "")
   169  		buf = []byte(tmp)
   170  
   171  		rawBodyStream := bytes.NewReader(buf)
   172  		em, err = email.NewEmailFromReader(rawBodyStream) // Parse with @jordanwright's library
   173  		if err != nil {
   174  			return emails, err
   175  		}
   176  
   177  		emtmp := Email{Email: em, SeqNum: msg.SeqNum} // Not sure why msg.Uid is always 0, so swapped to sequence numbers
   178  		emails = append(emails, emtmp)
   179  
   180  	}
   181  	return emails, nil
   182  }
   183  
   184  // newClient will initiate a new IMAP connection with the given creds.
   185  func (mbox *Mailbox) newClient() (*client.Client, error) {
   186  	var imapClient *client.Client
   187  	var err error
   188  	restrictedDialer := dialer.Dialer()
   189  	if mbox.TLS {
   190  		config := new(tls.Config)
   191  		config.InsecureSkipVerify = mbox.IgnoreCertErrors
   192  		imapClient, err = client.DialWithDialerTLS(restrictedDialer, mbox.Host, config)
   193  	} else {
   194  		imapClient, err = client.DialWithDialer(restrictedDialer, mbox.Host)
   195  	}
   196  	if err != nil {
   197  		return imapClient, err
   198  	}
   199  
   200  	err = imapClient.Login(mbox.User, mbox.Pwd)
   201  	if err != nil {
   202  		return imapClient, err
   203  	}
   204  
   205  	_, err = imapClient.Select(mbox.Folder, mbox.ReadOnly)
   206  	if err != nil {
   207  		return imapClient, err
   208  	}
   209  
   210  	return imapClient, nil
   211  }