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 }