Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for incoming emails #22056

Merged
merged 27 commits into from Jan 14, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5713613
Add support for incoming emails.
KN4CK3R Dec 6, 2022
47029bf
Merge branch 'main' of https://github.com/go-gitea/gitea into feature…
KN4CK3R Dec 7, 2022
e159bb8
Add comments.
KN4CK3R Dec 6, 2022
7c818d5
Fix merge error.
KN4CK3R Dec 7, 2022
ecb606d
Use enmime library.
KN4CK3R Dec 7, 2022
90860b6
Merge branch 'main' of https://github.com/go-gitea/gitea into feature…
KN4CK3R Dec 12, 2022
f7e74bb
Add example ini.
KN4CK3R Dec 12, 2022
b82efef
Fix merge errors.
KN4CK3R Dec 12, 2022
d85d448
Add missing import.
KN4CK3R Dec 14, 2022
f46ca3f
Merge branch 'main' of https://github.com/go-gitea/gitea into feature…
KN4CK3R Dec 14, 2022
ed3c24f
Merge branch 'main' of https://github.com/go-gitea/gitea into feature…
KN4CK3R Dec 19, 2022
28a34fe
Apply suggestions from code review
KN4CK3R Dec 20, 2022
80c1466
Log disallowed attachment name.
KN4CK3R Dec 20, 2022
f7ee50e
Merge branch 'main' of https://github.com/go-gitea/gitea into feature…
KN4CK3R Dec 20, 2022
35aa103
Merge branch 'main' into feature-incoming-email
lunny Dec 28, 2022
4e38af3
Merge branch 'main' of https://github.com/go-gitea/gitea into feature…
KN4CK3R Dec 29, 2022
5387897
Merge branch 'feature-incoming-email' of https://github.com/KN4CK3R/g…
KN4CK3R Dec 29, 2022
a334f55
Merge branch 'main' into feature-incoming-email
KN4CK3R Jan 2, 2023
1834a3f
Merge branch 'main' into feature-incoming-email
lunny Jan 3, 2023
0beb48a
Merge branch 'main' into feature-incoming-email
lunny Jan 3, 2023
6b2fe10
Merge branch 'main' of https://github.com/go-gitea/gitea into feature…
KN4CK3R Jan 11, 2023
b0a2c8f
Add suggested changes.
KN4CK3R Jan 13, 2023
5094c0d
Merge branch 'main' of https://github.com/go-gitea/gitea into feature…
KN4CK3R Jan 13, 2023
5066d29
Merge
KN4CK3R Jan 13, 2023
9c54fcf
Merge branch 'main' of https://github.com/go-gitea/gitea into feature…
KN4CK3R Jan 14, 2023
93b3469
Fix typo.
KN4CK3R Jan 14, 2023
661c367
return err
KN4CK3R Jan 14, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions .drone.yml
Expand Up @@ -230,6 +230,10 @@ services:
MINIO_ACCESS_KEY: 123456
MINIO_SECRET_KEY: 12345678

- name: smtpimap
image: tabascoterrier/docker-imap-devel:latest
pull: always

steps:
- name: fetch-tags
image: docker:git
Expand Down
14 changes: 14 additions & 0 deletions docs/content/doc/advanced/config-cheat-sheet.en-us.md
Expand Up @@ -743,6 +743,20 @@ and
- `SEND_BUFFER_LEN`: **100**: Buffer length of mailing queue. **DEPRECATED** use `LENGTH` in `[queue.mailer]`
- `SEND_AS_PLAIN_TEXT`: **false**: Send mails only in plain text, without HTML alternative.

## Incoming Email (`incoming_email`)

- `ENABLED`: **false**: Enable handling of incoming emails.
- `REPLY_TO_ADDRESS`: **\<empty\>**: # The email address including the %{token} placeholder that will be replaced per user/action. Example: `incoming+%{token}@example.com`. The placeholder must appear in the user part of the address (before the `@`).
KN4CK3R marked this conversation as resolved.
Show resolved Hide resolved
- `HOST`: **\<empty\>**: IMAP server host.
- `PORT`: **\<empty\>**: IMAP server port.
- `USERNAME`: **\<empty\>**: Username of the receiving account.
- `PASSWORD`: **\<empty\>**: Password of the receiving account.
- `USE_TLS`: **false**: Whether the IMAP server uses TLS.
- `SKIP_TLS_VERIFY`: **false**: If set to `true`, completely ignores server certificate validation errors. This option is unsafe.
- `MAILBOX`: **INBOX**: The mailbox name where incoming mail will end up.
- `DELETE_HANDLED_MESSAGE`: **true**: Whether handled messages should be deleted from the mailbox.
- `MAXIMUM_MESSAGE_SIZE`: **0**: Maximum size of a message to handle. Bigger messages are ignored.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, does this accept the <x>MB notation, or do you need to set an explicit byte size?
That's not clear from the description.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently it does not accept a human readable notation. I added that only for the packages. Should I make it available for other setting types too?


## Cache (`cache`)

- `ENABLED`: **true**: Enable the cache.
Expand Down
2 changes: 1 addition & 1 deletion docs/content/doc/features/comparison.en-us.md
Expand Up @@ -106,7 +106,7 @@ _Symbols used in table:_
| Issue search | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ |
| Global issue search | [/](https://github.com/go-gitea/gitea/issues/2434) | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ |
| Issue dependency | ✓ | ✘ | ✘ | ✘ | ✘ | ✘ | ✘ |
| Create issue via email | [✘](https://github.com/go-gitea/gitea/issues/6226) | ✘ | ✘ | | ✓ | ✓ | ✘ |
| Create issue via email | [✘](https://github.com/go-gitea/gitea/issues/6226) | ✘ | ✘ | | ✓ | ✓ | ✘ |
| Service Desk | [✘](https://github.com/go-gitea/gitea/issues/6219) | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ |

## Pull/Merge requests
Expand Down
2 changes: 1 addition & 1 deletion docs/content/doc/features/comparison.zh-cn.md
Expand Up @@ -92,7 +92,7 @@ _表格中的符号含义:_
| 工单搜索 | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ |
| 工单全局搜索 | [✘](https://github.com/go-gitea/gitea/issues/2434) | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ |
| 工单依赖关系 | ✓ | ✘ | ✘ | ✘ | ✘ | ✘ | ✘ |
| 通过 Email 创建工单 | [✘](https://github.com/go-gitea/gitea/issues/6226) | [✘](https://github.com/gogs/gogs/issues/2602) | ✘ | | ✓ | ✓ | ✘ |
| 通过 Email 创建工单 | [✘](https://github.com/go-gitea/gitea/issues/6226) | [✘](https://github.com/gogs/gogs/issues/2602) | ✘ | | ✓ | ✓ | ✘ |
| 服务台 | [✘](https://github.com/go-gitea/gitea/issues/6219) | ✘ | ✘ | [✓](https://gitlab.com/groups/gitlab-org/-/epics/3103) | ✓ | ✘ | ✘ |

#### Pull/Merge requests
Expand Down
2 changes: 1 addition & 1 deletion docs/content/doc/features/comparison.zh-tw.md
Expand Up @@ -93,7 +93,7 @@ menu:
| 問題搜尋 | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ |
| 全域問題搜尋 | [✘](https://github.com/go-gitea/gitea/issues/2434) | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ |
| 問題相依 | ✓ | ✘ | ✘ | ✘ | ✘ | ✘ | ✘ |
| 從電子郵件建立問題 | [✘](https://github.com/go-gitea/gitea/issues/6226) | [✘](https://github.com/gogs/gogs/issues/2602) | ✘ | | ✓ | ✓ | ✘ |
| 從電子郵件建立問題 | [✘](https://github.com/go-gitea/gitea/issues/6226) | [✘](https://github.com/gogs/gogs/issues/2602) | ✘ | | ✓ | ✓ | ✘ |
| 服務台 | [✘](https://github.com/go-gitea/gitea/issues/6219) | ✘ | ✘ | [✓](https://gitlab.com/groups/gitlab-org/-/epics/3103) | ✓ | ✘ | ✘ |

## 拉取/合併請求
Expand Down
47 changes: 47 additions & 0 deletions docs/content/doc/usage/incoming-email.en-us.md
@@ -0,0 +1,47 @@
---
date: "2022-12-01T00:00:00+00:00"
title: "Incoming Email"
slug: "incoming-email"
draft: false
toc: false
menu:
sidebar:
parent: "usage"
name: "Incoming Email"
weight: 13
identifier: "incoming-email"
---

# Incoming Email

Gitea supports the execution of several actions through incoming mails. This page describes how to set this up.

**Table of Contents**

{{< toc >}}

## Requirements

Handling incoming email messages requires an IMAP-enabled email account.
The recommended strategy is to use [email sub-addressing](https://en.wikipedia.org/wiki/Email_address#Sub-addressing) but a catch-all mailbox does work too.
The receiving email address contains a user/action specific token which tells Gitea which action should be performed.
This token is expected in the `To` and `Delivered-To` header fields.

Gitea tries to detect automatic responses to skip and the email server should be configured to reduce the incoming noise too (spam, newsletter).

## Configuration

To activate the handling of incoming email messages you have to configure the `incoming_email` section in the configuration file.

The `REPLY_TO_ADDRESS` contains the address an email client will respond to.
This address needs to contain the `%{token}` placeholder which will be replaced with a token describing the user/action.
This placeholder must only appear once in the address and must be in the user part of the address (before the `@`).

An example using email sub-addressing may look like this: `incoming+%{token}@example.com`

If a catch-all mailbox is used, the placeholder may be used anywhere in the user part of the address: `incoming+%{token}@example.com`, `incoming_%{token}@example.com`, `%{token}@example.com`

## Security

Be careful when choosing the domain used for receiving incoming email.
It's recommended receiving incoming email on a subdomain, such as `incoming.example.com` to prevent potential security problems with other services running on `example.com`.
5 changes: 5 additions & 0 deletions go.mod
Expand Up @@ -159,8 +159,13 @@ require (
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21 // indirect
github.com/dlclark/regexp2 v1.7.0 // indirect
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
github.com/emersion/go-imap v1.2.1 // indirect
github.com/emersion/go-message v0.16.0 // indirect
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1 // indirect
github.com/envoyproxy/protoc-gen-validate v0.6.2 // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect
Expand Down
12 changes: 12 additions & 0 deletions go.sum
Expand Up @@ -389,11 +389,14 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21 h1:PdsjTl0Cg+ZJgOx/CFV5NNgO1ThTreqdgKYiDCMHJwA=
github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21/go.mod h1:xJvkyD6Y2rZapGvPJLYo9dyx1s5dxBEDPa8T3YTuOk0=
github.com/djherbis/buffer v1.1.0/go.mod h1:VwN8VdFkMY0DCALdY8o00d3IZ6Amz/UNVMWcSaJT44o=
github.com/djherbis/buffer v1.2.0 h1:PH5Dd2ss0C7CRRhQCZ2u7MssF+No9ide8Ye71nPHcrQ=
github.com/djherbis/buffer v1.2.0/go.mod h1:fjnebbZjCUpPinBRD+TDwXSOeNQ7fPQWLfGQqiAiUyE=
github.com/djherbis/nio/v3 v3.0.1 h1:6wxhnuppteMa6RHA4L81Dq7ThkZH8SwnDzXDYy95vB4=
github.com/djherbis/nio/v3 v3.0.1/go.mod h1:Ng4h80pbZFMla1yKzm61cF0tqqilXZYrogmWgZxOcmg=
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo=
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
Expand All @@ -415,6 +418,15 @@ github.com/editorconfig/editorconfig-core-go/v2 v2.4.5 h1:kTcVMyCvFGQmTk0S5+R7GF
github.com/editorconfig/editorconfig-core-go/v2 v2.4.5/go.mod h1:rDB5UUleQsOI1HLbojaBmDNR8oUUe31InmNDTVzcDHY=
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA=
github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=
github.com/emersion/go-message v0.16.0 h1:uZLz8ClLv3V5fSFF/fFdW9jXjrZkXIpE1Fn8fKx7pO4=
github.com/emersion/go-message v0.16.0/go.mod h1:pDJDgf/xeUIF+eicT6B/hPX/ZbEorKkUMPOxrPVG2eQ=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
Expand Down
2 changes: 2 additions & 0 deletions models/unittest/testdb.go
Expand Up @@ -106,6 +106,8 @@ func MainTest(m *testing.M, testOpts *TestOptions) {

setting.Git.HomePath = filepath.Join(setting.AppDataPath, "home")

setting.IncomingEmail.ReplyToAddress = "incoming+%{token}@localhost"

if err = storage.Init(); err != nil {
fatalTestError("storage.Init: %v\n", err)
}
Expand Down
72 changes: 72 additions & 0 deletions modules/setting/incoming_email.go
@@ -0,0 +1,72 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package setting

import (
"fmt"
"net/mail"
"strings"

"code.gitea.io/gitea/modules/log"
)

var IncomingEmail = struct {
KN4CK3R marked this conversation as resolved.
Show resolved Hide resolved
Enabled bool
ReplyToAddress string
TokenPlaceholder string `ini:"-"`
Host string
Port int
UseTLS bool `ini:"USE_TLS"`
SkipTLSVerify bool `ini:"SKIP_TLS_VERIFY"`
Username string
Password string
Mailbox string
DeleteHandledMessage bool
MaximumMessageSize uint32
}{
Mailbox: "INBOX",
DeleteHandledMessage: true,
TokenPlaceholder: "%{token}",
}

func newIncomingEmail() {
if err := Cfg.Section("incoming_email").MapTo(&IncomingEmail); err != nil {
log.Fatal("Unable to map [incoming_email] section on to IncomingEmail. Error: %v", err)
}

if !IncomingEmail.Enabled {
return
}

if err := checkReplyToAddress(IncomingEmail.ReplyToAddress); err != nil {
log.Fatal("Invalid incoming_mail.REPLY_TO_ADDRESS (%s): %v", IncomingEmail.ReplyToAddress, err)
}
}

func checkReplyToAddress(address string) error {
parsed, err := mail.ParseAddress(IncomingEmail.ReplyToAddress)
if err != nil {
return err
}

if parsed.Name != "" {
return fmt.Errorf("name must not be set")
}

c := strings.Count(IncomingEmail.ReplyToAddress, IncomingEmail.TokenPlaceholder)
switch c {
case 0:
return fmt.Errorf("%s must appear in the user part of the address (before the @)", IncomingEmail.TokenPlaceholder)
case 1:
default:
return fmt.Errorf("%s must appear only once", IncomingEmail.TokenPlaceholder)
}

parts := strings.Split(IncomingEmail.ReplyToAddress, "@")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe parsed.Address is better?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Must be identical if the parsing worked without errors?

if !strings.Contains(parts[0], IncomingEmail.TokenPlaceholder) {
return fmt.Errorf("%s must appear in the user part of the address (before the @)", IncomingEmail.TokenPlaceholder)
}

return nil
}
1 change: 1 addition & 0 deletions modules/setting/setting.go
Expand Up @@ -1331,6 +1331,7 @@ func NewServices() {
newSessionService()
newCORSService()
newMailService()
newIncomingEmail()
newRegisterMailService()
newNotifyMailService()
newProxyService()
Expand Down
33 changes: 33 additions & 0 deletions modules/util/pack.go
@@ -0,0 +1,33 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package util

import (
"bytes"
"encoding/gob"
)

// PackData uses gob to encode the given data in sequence
func PackData(data ...interface{}) ([]byte, error) {
var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
for _, datum := range data {
if err := enc.Encode(datum); err != nil {
return nil, err
}
}
return buf.Bytes(), nil
}

// UnpackData uses gob to decode the given data in sequence
func UnpackData(buf []byte, data ...interface{}) error {
r := bytes.NewReader(buf)
enc := gob.NewDecoder(r)
Comment on lines +23 to +26
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we do something with r?
Returning it maybe?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line 26 uses it to decode the data.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know, but I don't quite understand the purpose of this method:
It is called UnpackData, so it should unpack the data, right?
As it only returns an error, the only option to return modified data is that it modifies the buf directly.
However, given the reader you declare, this looks unlikely to me…

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have a look at the test code. It's the same interface other Decode methods use. It uses the raw data in buf and unpacks it to the variables passed in data.

for _, datum := range data {
if err := enc.Decode(datum); err != nil {
return err
}
}
return nil
}
28 changes: 28 additions & 0 deletions modules/util/pack_test.go
@@ -0,0 +1,28 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package util

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestPackAndUnpackData(t *testing.T) {
s := "string"
i := int64(4)
f := float32(4.1)

var s2 string
var i2 int64
var f2 float32

data, err := PackData(s, i, f)
assert.NoError(t, err)

assert.NoError(t, UnpackData(data, &s2, &i2, &f2))
assert.NoError(t, UnpackData(data, &s2))
assert.Error(t, UnpackData(data, &i2))
assert.Error(t, UnpackData(data, &s2, &f2))
}
2 changes: 2 additions & 0 deletions routers/init.go
Expand Up @@ -40,6 +40,7 @@ import (
"code.gitea.io/gitea/services/automerge"
"code.gitea.io/gitea/services/cron"
"code.gitea.io/gitea/services/mailer"
mailer_incoming "code.gitea.io/gitea/services/mailer/incoming"
markup_service "code.gitea.io/gitea/services/markup"
repo_migrations "code.gitea.io/gitea/services/migrations"
mirror_service "code.gitea.io/gitea/services/mirror"
Expand Down Expand Up @@ -161,6 +162,7 @@ func GlobalInitInstalled(ctx context.Context) {
mustInit(task.Init)
mustInit(repo_migrations.Init)
eventsource.GetManager().Init()
mustInitCtx(ctx, mailer_incoming.Init)

mustInitCtx(ctx, syncAppConfForGit)

Expand Down