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  }