github.com/ArminBerberovic/gophish@v0.9.0/imap/imap.go (about) 1 package imap 2 3 // Functionality taken from https://github.com/jprobinson/eazye 4 5 import ( 6 "bytes" 7 "crypto/tls" 8 "fmt" 9 "strconv" 10 "time" 11 12 log "github.com/gophish/gophish/logger" 13 "github.com/gophish/gophish/models" 14 "github.com/jordan-wright/email" 15 "github.com/mxk/go-imap/imap" 16 ) 17 18 // Client interface for IMAP interactions 19 type Client interface { 20 Close(expunge bool) (cmd *imap.Command, err error) 21 Login(username, password string) (cmd *imap.Command, err error) 22 Logout(timeout time.Duration) (cmd *imap.Command, err error) 23 Select(mbox string, readonly bool) (cmd *imap.Command, err error) 24 UIDFetch(seq *imap.SeqSet, items ...string) (cmd *imap.Command, err error) 25 UIDSearch(spec ...imap.Field) (cmd *imap.Command, err error) 26 UIDStore(seq *imap.SeqSet, item string, value imap.Field) (cmd *imap.Command, err error) 27 } 28 29 // Email represents an email.Email with an included IMAP UID 30 type Email struct { 31 UID uint32 `json:"uid"` 32 *email.Email 33 } 34 35 // Mailbox holds onto the credentials and other information 36 // needed for connecting to an IMAP server. 37 type Mailbox struct { 38 Host string 39 TLS bool 40 User string 41 Pwd string 42 Folder string 43 // Read only mode, false (original logic) if not initialized 44 ReadOnly bool 45 } 46 47 // GetAll will pull all emails from the email folder and return them as a list. 48 func (mbox *Mailbox) GetAll(markAsRead, delete bool) ([]Email, error) { 49 // call chan, put 'em in a list, return 50 var emails []Email 51 responses, err := mbox.GenerateAll(markAsRead, delete) 52 if err != nil { 53 return emails, err 54 } 55 56 for resp := range responses { 57 if resp.Err != nil { 58 return emails, resp.Err 59 } 60 emails = append(emails, resp.Email) 61 } 62 63 return emails, nil 64 } 65 66 // GenerateAll will find all emails in the email folder and pass them along to the responses channel. 67 func (mbox *Mailbox) GenerateAll(markAsRead, delete bool) (chan Response, error) { 68 return mbox.generateMail("ALL", nil, markAsRead, delete) 69 } 70 71 // GetUnread will find all unread emails in the folder and return them as a list. 72 func (mbox *Mailbox) GetUnread(markAsRead, delete bool) ([]Email, error) { 73 // call chan, put 'em in a list, return 74 var emails []Email 75 76 responses, err := mbox.GenerateUnread(markAsRead, delete) 77 if err != nil { 78 return emails, err 79 } 80 81 for resp := range responses { 82 if resp.Err != nil { 83 return emails, resp.Err 84 } 85 emails = append(emails, resp.Email) 86 } 87 88 return emails, nil 89 } 90 91 // GenerateUnread will find all unread emails in the folder and pass them along to the responses channel. 92 func (mbox *Mailbox) GenerateUnread(markAsRead, delete bool) (chan Response, error) { 93 return mbox.generateMail("UNSEEN", nil, markAsRead, delete) 94 } 95 96 // MarkAsUnread will set the UNSEEN flag on a supplied slice of UIDs 97 func (mbox *Mailbox) MarkAsUnread(uids []uint32) error { 98 client, err := mbox.newClient() 99 if err != nil { 100 return err 101 } 102 defer func() { 103 client.Close(true) 104 client.Logout(30 * time.Second) 105 }() 106 for _, u := range uids { 107 err := alterEmail(client, u, "\\SEEN", false) 108 if err != nil { 109 return err //return on first failure 110 } 111 } 112 return nil 113 114 } 115 116 // DeleteEmails will delete emails from the supplied slice of UIDs 117 func (mbox *Mailbox) DeleteEmails(uids []uint32) error { 118 client, err := mbox.newClient() 119 if err != nil { 120 return err 121 } 122 defer func() { 123 client.Close(true) 124 client.Logout(30 * time.Second) 125 }() 126 for _, u := range uids { 127 err := deleteEmail(client, u) 128 if err != nil { 129 return err //return on first failure 130 } 131 } 132 return nil 133 134 } 135 136 // Validate validates supplied IMAP model by connecting to the server 137 func Validate(s *models.IMAP) error { 138 139 err := s.Validate() 140 if err != nil { 141 log.Error(err) 142 return err 143 } 144 145 s.Host = s.Host + ":" + strconv.Itoa(int(s.Port)) // Append port 146 mailServer := Mailbox{ 147 Host: s.Host, 148 TLS: s.TLS, 149 User: s.Username, 150 Pwd: s.Password, 151 Folder: s.Folder} 152 153 client, err := mailServer.newClient() 154 if err != nil { 155 log.Error(err.Error()) 156 } else { 157 client.Close(true) 158 client.Logout(30 * time.Second) 159 } 160 return err 161 } 162 163 // Response is a helper struct to wrap the email responses and possible errors. 164 type Response struct { 165 Email Email 166 Err error 167 } 168 169 // newClient will initiate a new IMAP connection with the given creds. 170 func (mbox *Mailbox) newClient() (*imap.Client, error) { 171 var client *imap.Client 172 var err error 173 if mbox.TLS { 174 client, err = imap.DialTLS(mbox.Host, new(tls.Config)) 175 if err != nil { 176 return client, err 177 } 178 } else { 179 client, err = imap.Dial(mbox.Host) 180 if err != nil { 181 return client, err 182 } 183 } 184 185 _, err = client.Login(mbox.User, mbox.Pwd) 186 if err != nil { 187 return client, err 188 } 189 190 _, err = imap.Wait(client.Select(mbox.Folder, mbox.ReadOnly)) 191 if err != nil { 192 return client, err 193 } 194 195 return client, nil 196 } 197 198 const dateFormat = "02-Jan-2006" 199 200 // findEmails will run a find the UIDs of any emails that match the search.: 201 func findEmails(client Client, search string, since *time.Time) (*imap.Command, error) { 202 var specs []imap.Field 203 if len(search) > 0 { 204 specs = append(specs, search) 205 } 206 207 if since != nil { 208 sinceStr := since.Format(dateFormat) 209 specs = append(specs, "SINCE", sinceStr) 210 } 211 212 // get headers and UID for UnSeen message in src inbox... 213 cmd, err := imap.Wait(client.UIDSearch(specs...)) 214 if err != nil { 215 return &imap.Command{}, fmt.Errorf("uid search failed: %s", err) 216 } 217 return cmd, nil 218 } 219 220 const GenerateBufferSize = 100 221 222 func (mbox *Mailbox) generateMail(search string, since *time.Time, markAsRead, delete bool) (chan Response, error) { 223 responses := make(chan Response, GenerateBufferSize) 224 client, err := mbox.newClient() 225 if err != nil { 226 close(responses) 227 return responses, fmt.Errorf("failed to create IMAP connection: %s", err) 228 } 229 230 go func() { 231 defer func() { 232 client.Close(true) 233 client.Logout(30 * time.Second) 234 close(responses) 235 }() 236 237 var cmd *imap.Command 238 // find all the UIDs 239 cmd, err = findEmails(client, search, since) 240 if err != nil { 241 responses <- Response{Err: err} 242 return 243 } 244 // gotta fetch 'em all 245 getEmails(client, cmd, markAsRead, delete, responses) 246 }() 247 248 return responses, nil 249 } 250 251 func getEmails(client Client, cmd *imap.Command, markAsRead, delete bool, responses chan Response) { 252 seq := &imap.SeqSet{} 253 msgCount := 0 254 for _, rsp := range cmd.Data { 255 for _, uid := range rsp.SearchResults() { 256 msgCount++ 257 seq.AddNum(uid) 258 } 259 } 260 261 if seq.Empty() { 262 return 263 } 264 265 fCmd, err := imap.Wait(client.UIDFetch(seq, "INTERNALDATE", "BODY[]", "UID", "RFC822.HEADER")) 266 if err != nil { 267 responses <- Response{Err: fmt.Errorf("unable to perform uid fetch: %s", err)} 268 return 269 } 270 271 var email Email 272 for _, msgData := range fCmd.Data { 273 msgFields := msgData.MessageInfo().Attrs 274 275 // make sure is a legit response before we attempt to parse it 276 // deal with unsolicited FETCH responses containing only flags 277 // I'm lookin' at YOU, Gmail! 278 // http://mailman13.u.washington.edu/pipermail/imap-protocol/2014-October/002355.html 279 // http://stackoverflow.com/questions/26262472/gmail-imap-is-sometimes-returning-bad-results-for-fetch 280 if _, ok := msgFields["RFC822.HEADER"]; !ok { 281 continue 282 } 283 284 email, err = NewEmail(msgFields) 285 if err != nil { 286 responses <- Response{Err: fmt.Errorf("unable to parse email: %s", err)} 287 return 288 } 289 290 responses <- Response{Email: email} 291 292 if !markAsRead { 293 err = removeSeen(client, imap.AsNumber(msgFields["UID"])) 294 if err != nil { 295 responses <- Response{Err: fmt.Errorf("unable to remove seen flag: %s", err)} 296 return 297 } 298 } 299 300 if delete { 301 err = deleteEmail(client, imap.AsNumber(msgFields["UID"])) 302 if err != nil { 303 responses <- Response{Err: fmt.Errorf("unable to delete email: %s", err)} 304 return 305 } 306 } 307 } 308 return 309 } 310 311 func deleteEmail(client Client, UID uint32) error { 312 return alterEmail(client, UID, "\\DELETED", true) 313 } 314 315 func removeSeen(client Client, UID uint32) error { 316 return alterEmail(client, UID, "\\SEEN", false) 317 } 318 319 func alterEmail(client Client, UID uint32, flag string, plus bool) error { 320 flg := "-FLAGS" 321 if plus { 322 flg = "+FLAGS" 323 } 324 fSeq := &imap.SeqSet{} 325 fSeq.AddNum(UID) 326 _, err := imap.Wait(client.UIDStore(fSeq, flg, flag)) 327 if err != nil { 328 return err 329 } 330 331 return nil 332 } 333 334 // NewEmail will parse an imap.FieldMap into an Email. This 335 // will expect the message to container the internaldate and the body with 336 // all headers included. 337 func NewEmail(msgFields imap.FieldMap) (Email, error) { 338 339 rawBody := imap.AsBytes(msgFields["BODY[]"]) 340 341 rawBodyStream := bytes.NewReader(rawBody) 342 em, err := email.NewEmailFromReader(rawBodyStream) // Parse with @jordanwright's library 343 if err != nil { 344 return Email{}, err 345 } 346 iem := Email{ 347 Email: em, 348 UID: imap.AsNumber(msgFields["UID"]), 349 } 350 351 return iem, err 352 }