code.gitea.io/gitea@v1.21.7/services/mailer/incoming/incoming.go (about) 1 // Copyright 2023 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package incoming 5 6 import ( 7 "context" 8 "crypto/tls" 9 "fmt" 10 net_mail "net/mail" 11 "regexp" 12 "strings" 13 "time" 14 15 "code.gitea.io/gitea/modules/log" 16 "code.gitea.io/gitea/modules/process" 17 "code.gitea.io/gitea/modules/setting" 18 "code.gitea.io/gitea/services/mailer/token" 19 20 "github.com/dimiro1/reply" 21 "github.com/emersion/go-imap" 22 "github.com/emersion/go-imap/client" 23 "github.com/jhillyerd/enmime" 24 ) 25 26 var ( 27 addressTokenRegex *regexp.Regexp 28 referenceTokenRegex *regexp.Regexp 29 ) 30 31 func Init(ctx context.Context) error { 32 if !setting.IncomingEmail.Enabled { 33 return nil 34 } 35 36 var err error 37 addressTokenRegex, err = regexp.Compile( 38 fmt.Sprintf( 39 `\A%s\z`, 40 strings.Replace(regexp.QuoteMeta(setting.IncomingEmail.ReplyToAddress), regexp.QuoteMeta(setting.IncomingEmail.TokenPlaceholder), "(.+)", 1), 41 ), 42 ) 43 if err != nil { 44 return err 45 } 46 referenceTokenRegex, err = regexp.Compile(fmt.Sprintf(`\Areply-(.+)@%s\z`, regexp.QuoteMeta(setting.Domain))) 47 if err != nil { 48 return err 49 } 50 51 go func() { 52 ctx, _, finished := process.GetManager().AddTypedContext(ctx, "Incoming Email", process.SystemProcessType, true) 53 defer finished() 54 55 // This background job processes incoming emails. It uses the IMAP IDLE command to get notified about incoming emails. 56 // The following loop restarts the processing logic after errors until ctx indicates to stop. 57 58 for { 59 select { 60 case <-ctx.Done(): 61 return 62 default: 63 if err := processIncomingEmails(ctx); err != nil { 64 log.Error("Error while processing incoming emails: %v", err) 65 } 66 select { 67 case <-ctx.Done(): 68 return 69 case <-time.NewTimer(10 * time.Second).C: 70 } 71 } 72 } 73 }() 74 75 return nil 76 } 77 78 // processIncomingEmails is the "main" method with the wait/process loop 79 func processIncomingEmails(ctx context.Context) error { 80 server := fmt.Sprintf("%s:%d", setting.IncomingEmail.Host, setting.IncomingEmail.Port) 81 82 var c *client.Client 83 var err error 84 if setting.IncomingEmail.UseTLS { 85 c, err = client.DialTLS(server, &tls.Config{InsecureSkipVerify: setting.IncomingEmail.SkipTLSVerify}) 86 } else { 87 c, err = client.Dial(server) 88 } 89 if err != nil { 90 return fmt.Errorf("could not connect to server '%s': %w", server, err) 91 } 92 93 if err := c.Login(setting.IncomingEmail.Username, setting.IncomingEmail.Password); err != nil { 94 return fmt.Errorf("could not login: %w", err) 95 } 96 defer func() { 97 if err := c.Logout(); err != nil { 98 log.Error("Logout from incoming email server failed: %v", err) 99 } 100 }() 101 102 if _, err := c.Select(setting.IncomingEmail.Mailbox, false); err != nil { 103 return fmt.Errorf("selecting box '%s' failed: %w", setting.IncomingEmail.Mailbox, err) 104 } 105 106 // The following loop processes messages. If there are no messages available, IMAP IDLE is used to wait for new messages. 107 // This process is repeated until an IMAP error occurs or ctx indicates to stop. 108 109 for { 110 select { 111 case <-ctx.Done(): 112 return nil 113 default: 114 if err := processMessages(ctx, c); err != nil { 115 return fmt.Errorf("could not process messages: %w", err) 116 } 117 if err := waitForUpdates(ctx, c); err != nil { 118 return fmt.Errorf("wait for updates failed: %w", err) 119 } 120 select { 121 case <-ctx.Done(): 122 return nil 123 case <-time.NewTimer(time.Second).C: 124 } 125 } 126 } 127 } 128 129 // waitForUpdates uses IMAP IDLE to wait for new emails 130 func waitForUpdates(ctx context.Context, c *client.Client) error { 131 updates := make(chan client.Update, 1) 132 133 c.Updates = updates 134 defer func() { 135 c.Updates = nil 136 }() 137 138 errs := make(chan error, 1) 139 stop := make(chan struct{}) 140 go func() { 141 errs <- c.Idle(stop, nil) 142 }() 143 144 stopped := false 145 for { 146 select { 147 case update := <-updates: 148 switch update.(type) { 149 case *client.MailboxUpdate: 150 if !stopped { 151 close(stop) 152 stopped = true 153 } 154 default: 155 } 156 case err := <-errs: 157 if err != nil { 158 return fmt.Errorf("imap idle failed: %w", err) 159 } 160 return nil 161 case <-ctx.Done(): 162 return nil 163 } 164 } 165 } 166 167 // processMessages searches unread mails and processes them. 168 func processMessages(ctx context.Context, c *client.Client) error { 169 criteria := imap.NewSearchCriteria() 170 criteria.WithoutFlags = []string{imap.SeenFlag} 171 criteria.Smaller = setting.IncomingEmail.MaximumMessageSize 172 ids, err := c.Search(criteria) 173 if err != nil { 174 return fmt.Errorf("imap search failed: %w", err) 175 } 176 177 if len(ids) == 0 { 178 return nil 179 } 180 181 seqset := new(imap.SeqSet) 182 seqset.AddNum(ids...) 183 messages := make(chan *imap.Message, 10) 184 185 section := &imap.BodySectionName{} 186 187 errs := make(chan error, 1) 188 go func() { 189 errs <- c.Fetch( 190 seqset, 191 []imap.FetchItem{section.FetchItem()}, 192 messages, 193 ) 194 }() 195 196 handledSet := new(imap.SeqSet) 197 loop: 198 for { 199 select { 200 case <-ctx.Done(): 201 break loop 202 case msg, ok := <-messages: 203 if !ok { 204 if setting.IncomingEmail.DeleteHandledMessage && !handledSet.Empty() { 205 if err := c.Store( 206 handledSet, 207 imap.FormatFlagsOp(imap.AddFlags, true), 208 []any{imap.DeletedFlag}, 209 nil, 210 ); err != nil { 211 return fmt.Errorf("imap store failed: %w", err) 212 } 213 214 if err := c.Expunge(nil); err != nil { 215 return fmt.Errorf("imap expunge failed: %w", err) 216 } 217 } 218 return nil 219 } 220 221 err := func() error { 222 r := msg.GetBody(section) 223 if r == nil { 224 return fmt.Errorf("could not get body from message: %w", err) 225 } 226 227 env, err := enmime.ReadEnvelope(r) 228 if err != nil { 229 return fmt.Errorf("could not read envelope: %w", err) 230 } 231 232 if isAutomaticReply(env) { 233 log.Debug("Skipping automatic email reply") 234 return nil 235 } 236 237 t := searchTokenInHeaders(env) 238 if t == "" { 239 log.Debug("Incoming email token not found in headers") 240 return nil 241 } 242 243 handlerType, user, payload, err := token.ExtractToken(ctx, t) 244 if err != nil { 245 if _, ok := err.(*token.ErrToken); ok { 246 log.Info("Invalid incoming email token: %v", err) 247 return nil 248 } 249 return err 250 } 251 252 handler, ok := handlers[handlerType] 253 if !ok { 254 return fmt.Errorf("unexpected handler type: %v", handlerType) 255 } 256 257 content := getContentFromMailReader(env) 258 259 if err := handler.Handle(ctx, content, user, payload); err != nil { 260 return fmt.Errorf("could not handle message: %w", err) 261 } 262 263 handledSet.AddNum(msg.SeqNum) 264 265 return nil 266 }() 267 if err != nil { 268 log.Error("Error while processing incoming email[%v]: %v", msg.Uid, err) 269 } 270 } 271 } 272 273 if err := <-errs; err != nil { 274 return fmt.Errorf("imap fetch failed: %w", err) 275 } 276 277 return nil 278 } 279 280 // isAutomaticReply tests if the headers indicate an automatic reply 281 func isAutomaticReply(env *enmime.Envelope) bool { 282 autoSubmitted := env.GetHeader("Auto-Submitted") 283 if autoSubmitted != "" && autoSubmitted != "no" { 284 return true 285 } 286 autoReply := env.GetHeader("X-Autoreply") 287 if autoReply == "yes" { 288 return true 289 } 290 autoRespond := env.GetHeader("X-Autorespond") 291 return autoRespond != "" 292 } 293 294 // searchTokenInHeaders looks for the token in To, Delivered-To and References 295 func searchTokenInHeaders(env *enmime.Envelope) string { 296 if addressTokenRegex != nil { 297 to, _ := env.AddressList("To") 298 299 token := searchTokenInAddresses(to) 300 if token != "" { 301 return token 302 } 303 304 deliveredTo, _ := env.AddressList("Delivered-To") 305 306 token = searchTokenInAddresses(deliveredTo) 307 if token != "" { 308 return token 309 } 310 } 311 312 references := env.GetHeader("References") 313 for { 314 begin := strings.IndexByte(references, '<') 315 if begin == -1 { 316 break 317 } 318 begin++ 319 320 end := strings.IndexByte(references, '>') 321 if end == -1 || begin > end { 322 break 323 } 324 325 match := referenceTokenRegex.FindStringSubmatch(references[begin:end]) 326 if len(match) == 2 { 327 return match[1] 328 } 329 330 references = references[end+1:] 331 } 332 333 return "" 334 } 335 336 // searchTokenInAddresses looks for the token in an address 337 func searchTokenInAddresses(addresses []*net_mail.Address) string { 338 for _, address := range addresses { 339 match := addressTokenRegex.FindStringSubmatch(address.Address) 340 if len(match) != 2 { 341 continue 342 } 343 344 return match[1] 345 } 346 347 return "" 348 } 349 350 type MailContent struct { 351 Content string 352 Attachments []*Attachment 353 } 354 355 type Attachment struct { 356 Name string 357 Content []byte 358 } 359 360 // getContentFromMailReader grabs the plain content and the attachments from the mail. 361 // A potential reply/signature gets stripped from the content. 362 func getContentFromMailReader(env *enmime.Envelope) *MailContent { 363 attachments := make([]*Attachment, 0, len(env.Attachments)) 364 for _, attachment := range env.Attachments { 365 attachments = append(attachments, &Attachment{ 366 Name: attachment.FileName, 367 Content: attachment.Content, 368 }) 369 } 370 371 return &MailContent{ 372 Content: reply.FromText(env.Text), 373 Attachments: attachments, 374 } 375 }