From 404ae4975ece7948d5cbb9b154fc993acfb6008d Mon Sep 17 00:00:00 2001 From: a1012112796 <1012112796@qq.com> Date: Mon, 16 Nov 2020 21:49:14 +0800 Subject: [PATCH 01/21] reply issue by email Signed-off-by: a1012112796 <1012112796@qq.com> --- custom/conf/app.example.ini | 18 + go.mod | 2 + go.sum | 11 + modules/cron/tasks_extended.go | 12 + modules/setting/mail_recive.go | 46 + modules/setting/setting.go | 1 + routers/init.go | 2 + services/imap/cron.go | 46 + services/imap/imap.go | 365 +++++ services/imap/mail_reciver.go | 162 +++ services/mailer/mail.go | 5 + templates/mail/issue/default.tmpl | 2 +- vendor/github.com/emersion/go-imap/.build.yml | 19 + vendor/github.com/emersion/go-imap/.gitignore | 28 + vendor/github.com/emersion/go-imap/LICENSE | 23 + vendor/github.com/emersion/go-imap/README.md | 170 +++ .../emersion/go-imap/client/client.go | 689 ++++++++++ .../emersion/go-imap/client/cmd_any.go | 87 ++ .../emersion/go-imap/client/cmd_auth.go | 254 ++++ .../emersion/go-imap/client/cmd_noauth.go | 174 +++ .../emersion/go-imap/client/cmd_selected.go | 267 ++++ .../github.com/emersion/go-imap/client/tag.go | 24 + vendor/github.com/emersion/go-imap/command.go | 57 + .../emersion/go-imap/commands/append.go | 93 ++ .../emersion/go-imap/commands/authenticate.go | 124 ++ .../emersion/go-imap/commands/capability.go | 18 + .../emersion/go-imap/commands/check.go | 18 + .../emersion/go-imap/commands/close.go | 18 + .../emersion/go-imap/commands/commands.go | 2 + .../emersion/go-imap/commands/copy.go | 47 + .../emersion/go-imap/commands/create.go | 38 + .../emersion/go-imap/commands/delete.go | 38 + .../emersion/go-imap/commands/expunge.go | 16 + .../emersion/go-imap/commands/fetch.go | 55 + .../emersion/go-imap/commands/list.go | 60 + .../emersion/go-imap/commands/login.go | 36 + .../emersion/go-imap/commands/logout.go | 18 + .../emersion/go-imap/commands/noop.go | 18 + .../emersion/go-imap/commands/rename.go | 51 + .../emersion/go-imap/commands/search.go | 57 + .../emersion/go-imap/commands/select.go | 45 + .../emersion/go-imap/commands/starttls.go | 18 + .../emersion/go-imap/commands/status.go | 58 + .../emersion/go-imap/commands/store.go | 50 + .../emersion/go-imap/commands/subscribe.go | 63 + .../emersion/go-imap/commands/uid.go | 44 + vendor/github.com/emersion/go-imap/conn.go | 284 ++++ vendor/github.com/emersion/go-imap/date.go | 71 + vendor/github.com/emersion/go-imap/go.mod | 9 + vendor/github.com/emersion/go-imap/go.sum | 20 + vendor/github.com/emersion/go-imap/imap.go | 106 ++ vendor/github.com/emersion/go-imap/literal.go | 13 + vendor/github.com/emersion/go-imap/logger.go | 8 + vendor/github.com/emersion/go-imap/mailbox.go | 271 ++++ vendor/github.com/emersion/go-imap/message.go | 1183 +++++++++++++++++ vendor/github.com/emersion/go-imap/read.go | 467 +++++++ .../github.com/emersion/go-imap/response.go | 181 +++ .../go-imap/responses/authenticate.go | 61 + .../emersion/go-imap/responses/capability.go | 20 + .../emersion/go-imap/responses/expunge.go | 43 + .../emersion/go-imap/responses/fetch.go | 47 + .../emersion/go-imap/responses/list.go | 57 + .../emersion/go-imap/responses/responses.go | 35 + .../emersion/go-imap/responses/search.go | 41 + .../emersion/go-imap/responses/select.go | 142 ++ .../emersion/go-imap/responses/status.go | 53 + vendor/github.com/emersion/go-imap/search.go | 371 ++++++ vendor/github.com/emersion/go-imap/seqset.go | 289 ++++ vendor/github.com/emersion/go-imap/status.go | 136 ++ .../emersion/go-imap/utf7/decoder.go | 151 +++ .../emersion/go-imap/utf7/encoder.go | 91 ++ .../github.com/emersion/go-imap/utf7/utf7.go | 34 + vendor/github.com/emersion/go-imap/write.go | 255 ++++ .../github.com/emersion/go-message/.build.yml | 19 + .../github.com/emersion/go-message/.gitignore | 24 + vendor/github.com/emersion/go-message/LICENSE | 21 + .../github.com/emersion/go-message/README.md | 32 + .../github.com/emersion/go-message/charset.go | 66 + .../emersion/go-message/encoding.go | 68 + .../github.com/emersion/go-message/entity.go | 129 ++ vendor/github.com/emersion/go-message/go.mod | 11 + vendor/github.com/emersion/go-message/go.sum | 16 + .../github.com/emersion/go-message/header.go | 103 ++ .../emersion/go-message/mail/address.go | 46 + .../emersion/go-message/mail/attachment.go | 30 + .../emersion/go-message/mail/header.go | 339 +++++ .../emersion/go-message/mail/inline.go | 10 + .../emersion/go-message/mail/mail.go | 9 + .../emersion/go-message/mail/reader.go | 130 ++ .../emersion/go-message/mail/writer.go | 126 ++ .../github.com/emersion/go-message/message.go | 12 + .../emersion/go-message/multipart.go | 116 ++ .../emersion/go-message/textproto/header.go | 658 +++++++++ .../go-message/textproto/multipart.go | 473 +++++++ .../go-message/textproto/textproto.go | 2 + .../github.com/emersion/go-message/writer.go | 127 ++ vendor/github.com/emersion/go-sasl/.gitignore | 24 + .../github.com/emersion/go-sasl/.travis.yml | 3 + vendor/github.com/emersion/go-sasl/LICENSE | 21 + vendor/github.com/emersion/go-sasl/README.md | 18 + .../github.com/emersion/go-sasl/anonymous.go | 56 + .../github.com/emersion/go-sasl/external.go | 26 + vendor/github.com/emersion/go-sasl/login.go | 89 ++ .../emersion/go-sasl/oauthbearer.go | 63 + vendor/github.com/emersion/go-sasl/plain.go | 77 ++ vendor/github.com/emersion/go-sasl/sasl.go | 45 + vendor/github.com/emersion/go-sasl/xoauth2.go | 48 + .../emersion/go-textwrapper/.gitignore | 24 + .../emersion/go-textwrapper/.travis.yml | 1 + .../emersion/go-textwrapper/LICENSE | 21 + .../emersion/go-textwrapper/README.md | 27 + .../emersion/go-textwrapper/wrapper.go | 61 + .../martinlindhe/base36/.travis.yml | 12 + vendor/github.com/martinlindhe/base36/LICENSE | 21 + .../github.com/martinlindhe/base36/Makefile | 5 + .../github.com/martinlindhe/base36/README.md | 29 + .../github.com/martinlindhe/base36/base36.go | 125 ++ vendor/github.com/unknwon/com/go.mod | 2 + vendor/modules.txt | 18 + 119 files changed, 11270 insertions(+), 1 deletion(-) create mode 100644 modules/setting/mail_recive.go create mode 100644 services/imap/cron.go create mode 100644 services/imap/imap.go create mode 100644 services/imap/mail_reciver.go create mode 100644 vendor/github.com/emersion/go-imap/.build.yml create mode 100644 vendor/github.com/emersion/go-imap/.gitignore create mode 100644 vendor/github.com/emersion/go-imap/LICENSE create mode 100644 vendor/github.com/emersion/go-imap/README.md create mode 100644 vendor/github.com/emersion/go-imap/client/client.go create mode 100644 vendor/github.com/emersion/go-imap/client/cmd_any.go create mode 100644 vendor/github.com/emersion/go-imap/client/cmd_auth.go create mode 100644 vendor/github.com/emersion/go-imap/client/cmd_noauth.go create mode 100644 vendor/github.com/emersion/go-imap/client/cmd_selected.go create mode 100644 vendor/github.com/emersion/go-imap/client/tag.go create mode 100644 vendor/github.com/emersion/go-imap/command.go create mode 100644 vendor/github.com/emersion/go-imap/commands/append.go create mode 100644 vendor/github.com/emersion/go-imap/commands/authenticate.go create mode 100644 vendor/github.com/emersion/go-imap/commands/capability.go create mode 100644 vendor/github.com/emersion/go-imap/commands/check.go create mode 100644 vendor/github.com/emersion/go-imap/commands/close.go create mode 100644 vendor/github.com/emersion/go-imap/commands/commands.go create mode 100644 vendor/github.com/emersion/go-imap/commands/copy.go create mode 100644 vendor/github.com/emersion/go-imap/commands/create.go create mode 100644 vendor/github.com/emersion/go-imap/commands/delete.go create mode 100644 vendor/github.com/emersion/go-imap/commands/expunge.go create mode 100644 vendor/github.com/emersion/go-imap/commands/fetch.go create mode 100644 vendor/github.com/emersion/go-imap/commands/list.go create mode 100644 vendor/github.com/emersion/go-imap/commands/login.go create mode 100644 vendor/github.com/emersion/go-imap/commands/logout.go create mode 100644 vendor/github.com/emersion/go-imap/commands/noop.go create mode 100644 vendor/github.com/emersion/go-imap/commands/rename.go create mode 100644 vendor/github.com/emersion/go-imap/commands/search.go create mode 100644 vendor/github.com/emersion/go-imap/commands/select.go create mode 100644 vendor/github.com/emersion/go-imap/commands/starttls.go create mode 100644 vendor/github.com/emersion/go-imap/commands/status.go create mode 100644 vendor/github.com/emersion/go-imap/commands/store.go create mode 100644 vendor/github.com/emersion/go-imap/commands/subscribe.go create mode 100644 vendor/github.com/emersion/go-imap/commands/uid.go create mode 100644 vendor/github.com/emersion/go-imap/conn.go create mode 100644 vendor/github.com/emersion/go-imap/date.go create mode 100644 vendor/github.com/emersion/go-imap/go.mod create mode 100644 vendor/github.com/emersion/go-imap/go.sum create mode 100644 vendor/github.com/emersion/go-imap/imap.go create mode 100644 vendor/github.com/emersion/go-imap/literal.go create mode 100644 vendor/github.com/emersion/go-imap/logger.go create mode 100644 vendor/github.com/emersion/go-imap/mailbox.go create mode 100644 vendor/github.com/emersion/go-imap/message.go create mode 100644 vendor/github.com/emersion/go-imap/read.go create mode 100644 vendor/github.com/emersion/go-imap/response.go create mode 100644 vendor/github.com/emersion/go-imap/responses/authenticate.go create mode 100644 vendor/github.com/emersion/go-imap/responses/capability.go create mode 100644 vendor/github.com/emersion/go-imap/responses/expunge.go create mode 100644 vendor/github.com/emersion/go-imap/responses/fetch.go create mode 100644 vendor/github.com/emersion/go-imap/responses/list.go create mode 100644 vendor/github.com/emersion/go-imap/responses/responses.go create mode 100644 vendor/github.com/emersion/go-imap/responses/search.go create mode 100644 vendor/github.com/emersion/go-imap/responses/select.go create mode 100644 vendor/github.com/emersion/go-imap/responses/status.go create mode 100644 vendor/github.com/emersion/go-imap/search.go create mode 100644 vendor/github.com/emersion/go-imap/seqset.go create mode 100644 vendor/github.com/emersion/go-imap/status.go create mode 100644 vendor/github.com/emersion/go-imap/utf7/decoder.go create mode 100644 vendor/github.com/emersion/go-imap/utf7/encoder.go create mode 100644 vendor/github.com/emersion/go-imap/utf7/utf7.go create mode 100644 vendor/github.com/emersion/go-imap/write.go create mode 100644 vendor/github.com/emersion/go-message/.build.yml create mode 100644 vendor/github.com/emersion/go-message/.gitignore create mode 100644 vendor/github.com/emersion/go-message/LICENSE create mode 100644 vendor/github.com/emersion/go-message/README.md create mode 100644 vendor/github.com/emersion/go-message/charset.go create mode 100644 vendor/github.com/emersion/go-message/encoding.go create mode 100644 vendor/github.com/emersion/go-message/entity.go create mode 100644 vendor/github.com/emersion/go-message/go.mod create mode 100644 vendor/github.com/emersion/go-message/go.sum create mode 100644 vendor/github.com/emersion/go-message/header.go create mode 100644 vendor/github.com/emersion/go-message/mail/address.go create mode 100644 vendor/github.com/emersion/go-message/mail/attachment.go create mode 100644 vendor/github.com/emersion/go-message/mail/header.go create mode 100644 vendor/github.com/emersion/go-message/mail/inline.go create mode 100644 vendor/github.com/emersion/go-message/mail/mail.go create mode 100644 vendor/github.com/emersion/go-message/mail/reader.go create mode 100644 vendor/github.com/emersion/go-message/mail/writer.go create mode 100644 vendor/github.com/emersion/go-message/message.go create mode 100644 vendor/github.com/emersion/go-message/multipart.go create mode 100644 vendor/github.com/emersion/go-message/textproto/header.go create mode 100644 vendor/github.com/emersion/go-message/textproto/multipart.go create mode 100644 vendor/github.com/emersion/go-message/textproto/textproto.go create mode 100644 vendor/github.com/emersion/go-message/writer.go create mode 100644 vendor/github.com/emersion/go-sasl/.gitignore create mode 100644 vendor/github.com/emersion/go-sasl/.travis.yml create mode 100644 vendor/github.com/emersion/go-sasl/LICENSE create mode 100644 vendor/github.com/emersion/go-sasl/README.md create mode 100644 vendor/github.com/emersion/go-sasl/anonymous.go create mode 100644 vendor/github.com/emersion/go-sasl/external.go create mode 100644 vendor/github.com/emersion/go-sasl/login.go create mode 100644 vendor/github.com/emersion/go-sasl/oauthbearer.go create mode 100644 vendor/github.com/emersion/go-sasl/plain.go create mode 100644 vendor/github.com/emersion/go-sasl/sasl.go create mode 100644 vendor/github.com/emersion/go-sasl/xoauth2.go create mode 100644 vendor/github.com/emersion/go-textwrapper/.gitignore create mode 100644 vendor/github.com/emersion/go-textwrapper/.travis.yml create mode 100644 vendor/github.com/emersion/go-textwrapper/LICENSE create mode 100644 vendor/github.com/emersion/go-textwrapper/README.md create mode 100644 vendor/github.com/emersion/go-textwrapper/wrapper.go create mode 100644 vendor/github.com/martinlindhe/base36/.travis.yml create mode 100644 vendor/github.com/martinlindhe/base36/LICENSE create mode 100644 vendor/github.com/martinlindhe/base36/Makefile create mode 100644 vendor/github.com/martinlindhe/base36/README.md create mode 100644 vendor/github.com/martinlindhe/base36/base36.go diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index a4e35d2495f52..aee7ff998113b 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -729,6 +729,24 @@ SENDMAIL_ARGS = ; Timeout for Sendmail SENDMAIL_TIMEOUT = 5m +[mail_recive] +ENABLED = false +; Buffer length of channel, keep it as it is if you don't know what it is. +READ_BUFFER_LEN = 100 +; email address to recive mail +RECIVE_EMAIL = +; recive email box +RECIVE_BOX = INBOX +; Mail server +; Gmail: imap.gmail.com:993 +; QQ: imap.qq.com:993 +HOST = +USER = +PASSWD = +IS_TLS_ENABLED = true +; delete mail after rode +DELETE_RODE_MAIL = false + [cache] ; if the cache enabled ENABLED = true diff --git a/go.mod b/go.mod index e9a264fdfdf18..fc83b0124bc40 100644 --- a/go.mod +++ b/go.mod @@ -32,6 +32,8 @@ require ( github.com/dlclark/regexp2 v1.4.0 // indirect github.com/dustin/go-humanize v1.0.0 github.com/editorconfig/editorconfig-core-go/v2 v2.3.8 + github.com/emersion/go-imap v1.0.6 + github.com/emersion/go-message v0.13.0 github.com/emirpasic/gods v1.12.0 github.com/ethantkoenig/rupture v0.0.0-20181029165146-c3b3b810dc77 github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 // indirect diff --git a/go.sum b/go.sum index c169da1c76157..b7e7cf4715e45 100644 --- a/go.sum +++ b/go.sum @@ -281,6 +281,15 @@ github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFP github.com/editorconfig/editorconfig-core-go/v2 v2.3.8 h1:nq6QPrFjoI1QP9trhj+bsXoS8MSjhTgQXgTavA5zPbg= github.com/editorconfig/editorconfig-core-go/v2 v2.3.8/go.mod h1:z7TIMh40583cev3v8ei7V1RRPKeHQbttoa4Vm5/5u7g= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/emersion/go-imap v1.0.6 h1:N9+o5laOGuntStBo+BOgfEB5evPsPD+K5+M0T2dctIc= +github.com/emersion/go-imap v1.0.6/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU= +github.com/emersion/go-message v0.11.1/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY= +github.com/emersion/go-message v0.13.0 h1:R4+CZv4Msxfk9tMaERjMkapdvdO2faWLuB5KHFsNLZE= +github.com/emersion/go-message v0.13.0/go.mod h1:kYIioST9GDHte9/BRWgi93rpqbDuFftMjKSMaXS8ABo= +github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b h1:uhWtEWBHgop1rqEk2klKaxPAkVDCXexai6hSuRQ7Nvs= +github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k= +github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe h1:40SWqY0zE3qCi6ZrtTf5OUdNm5lDnGnjRSq9GgmeTrg= +github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= @@ -761,6 +770,8 @@ github.com/markbates/goth v1.65.0 h1:IbXpMneUhqbxgJ8JP1Ghl8ghlAaVX66jWDAapU1KxqU github.com/markbates/goth v1.65.0/go.mod h1:65frybxoeSCfORin51KOKqAKbIh7wREIDvdCkdWj//4= github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= +github.com/martinlindhe/base36 v1.0.0 h1:eYsumTah144C0A8P1T/AVSUk5ZoLnhfYFM3OGQxB52A= +github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= diff --git a/modules/cron/tasks_extended.go b/modules/cron/tasks_extended.go index f0742eb471f36..5e08712c3528d 100644 --- a/modules/cron/tasks_extended.go +++ b/modules/cron/tasks_extended.go @@ -11,6 +11,7 @@ import ( "code.gitea.io/gitea/models" repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/services/imap" ) func registerDeleteInactiveUsers() { @@ -117,6 +118,16 @@ func registerRemoveRandomAvatars() { }) } +func registerImapFetchUnReadMails() { + RegisterTaskFatal("imap_fetch_mails", &BaseConfig{ + Enabled: setting.MailReciveService != nil, + RunAtStart: true, + Schedule: "@every 5m", + }, func(ctx context.Context, _ *models.User, _ Config) error { + return imap.FetchAllUnReadMails() + }) +} + func initExtendedTasks() { registerDeleteInactiveUsers() registerDeleteRepositoryArchives() @@ -127,4 +138,5 @@ func initExtendedTasks() { registerReinitMissingRepositories() registerDeleteMissingRepositories() registerRemoveRandomAvatars() + registerImapFetchUnReadMails() } diff --git a/modules/setting/mail_recive.go b/modules/setting/mail_recive.go new file mode 100644 index 0000000000000..78e734fbd9175 --- /dev/null +++ b/modules/setting/mail_recive.go @@ -0,0 +1,46 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package setting + +import "code.gitea.io/gitea/modules/log" + +// MailReciver represents mail recive service. +type MailReciver struct { + ReciveEmail string + ReciveBox string + QueueLength int + + Host string + User, Passwd string + IsTLSEnabled bool + + DeleteRodeMail bool +} + +var ( + // MailReciveService mail recive config + MailReciveService *MailReciver +) + +func newMailReciveService() { + sec := Cfg.Section("mail_recive") + // Check mailer setting. + if !sec.Key("ENABLED").MustBool() { + return + } + + MailReciveService = &MailReciver{ + ReciveEmail: sec.Key("RECIVE_EMAIL").String(), + ReciveBox: sec.Key("RECIVE_BOX").MustString("INBOX"), + QueueLength: sec.Key("READ_BUFFER_LEN").MustInt(100), + Host: sec.Key("HOST").String(), + User: sec.Key("USER").String(), + Passwd: sec.Key("PASSWD").String(), + IsTLSEnabled: sec.Key("IS_TLS_ENABLED").MustBool(true), + DeleteRodeMail: sec.Key("DELETE_RODE_MAIL").MustBool(false), + } + + log.Info("Mail Recive Service Enabled") +} diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 7ae8bb352de10..156cd3e176adf 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -1030,6 +1030,7 @@ func NewServices() { newSessionService() newCORSService() newMailService() + newMailReciveService() newRegisterMailService() newNotifyMailService() newWebhookService() diff --git a/routers/init.go b/routers/init.go index 702acb7260907..2a1d1ff830755 100644 --- a/routers/init.go +++ b/routers/init.go @@ -32,6 +32,7 @@ import ( "code.gitea.io/gitea/modules/svg" "code.gitea.io/gitea/modules/task" "code.gitea.io/gitea/modules/webhook" + "code.gitea.io/gitea/services/imap" "code.gitea.io/gitea/services/mailer" mirror_service "code.gitea.io/gitea/services/mirror" pull_service "code.gitea.io/gitea/services/pull" @@ -63,6 +64,7 @@ func NewServices() { log.Fatal("repository init failed: %v", err) } mailer.NewContext() + imap.NewContext() _ = cache.NewContext() notification.NewContext() } diff --git a/services/imap/cron.go b/services/imap/cron.go new file mode 100644 index 0000000000000..3c0925ba9aa01 --- /dev/null +++ b/services/imap/cron.go @@ -0,0 +1,46 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package imap + +import ( + "code.gitea.io/gitea/modules/setting" +) + +var ( + c *Client +) + +// FetchAllUnReadMails fetch all unread mails +func FetchAllUnReadMails() (err error) { + if c == nil { + c, err = NewImapClient(ClientInitOpt{ + Addr: setting.MailReciveService.Host, + UserName: setting.MailReciveService.User, + Passwd: setting.MailReciveService.Passwd, + IsTLS: setting.MailReciveService.IsTLSEnabled, + }) + if err != nil { + return + } + } + + if !mailReadQueue.IsEmpty() { + return + } + + mails, err := c.GetUnReadMails(setting.MailReciveService.ReciveBox, 100) + if err != nil { + return + } + + for _, mail := range mails { + err = mailReadQueue.Push(mail) + if err != nil { + return err + } + } + + return nil +} diff --git a/services/imap/imap.go b/services/imap/imap.go new file mode 100644 index 0000000000000..0836ad2b82b6b --- /dev/null +++ b/services/imap/imap.go @@ -0,0 +1,365 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package imap + +import ( + "errors" + "io" + "sync" + "time" + + "code.gitea.io/gitea/modules/log" + + "github.com/PuerkitoBio/goquery" + "github.com/emersion/go-imap" + "github.com/emersion/go-imap/client" + "github.com/emersion/go-message/mail" +) + +// Client an imap clientor +type Client struct { + Client *client.Client + UserName string + Passwd string + Addr string + IsTLS bool + Lock sync.Mutex +} + +// ClientInitOpt options to init an Client +type ClientInitOpt struct { + Addr string + UserName string + Passwd string + IsTLS bool +} + +// NewImapClient init a imap Client +func NewImapClient(opt ClientInitOpt) (c *Client, err error) { + c = new(Client) + + c.UserName = opt.UserName + c.Passwd = opt.Passwd + c.Addr = opt.Addr + c.IsTLS = opt.IsTLS + + // try login + if err = c.Login(); err != nil { + return nil, err + } + + if err = c.LogOut(); err != nil { + return nil, err + } + + return c, nil +} + +// Login login to service +func (c *Client) Login() error { + var err error + + c.Lock.Lock() + + // Connect to server + if c.IsTLS { + c.Client, err = client.DialTLS(c.Addr, nil) + } else { + c.Client, err = client.Dial(c.Addr) + } + if err != nil { + return err + } + + return c.Client.Login(c.UserName, c.Passwd) +} + +// LogOut LogOut from service +func (c *Client) LogOut() error { + err := c.Client.Logout() + c.Client = nil + c.Lock.Unlock() + return err +} + +// GetUnReadMailIDs get all unread mails +func (c *Client) GetUnReadMailIDs(mailBox string) ([]uint32, error) { + if err := c.Login(); err != nil { + return nil, err + } + defer func() { + err := c.LogOut() + if err != nil { + log.Warn("Imap.Logout", err) + } + }() + + if len(mailBox) == 0 { + mailBox = "INBOX" + } + + // Select mail box + _, err := c.Client.Select(mailBox, false) + if err != nil { + return nil, err + } + + // Set search criteria + criteria := imap.NewSearchCriteria() + criteria.WithoutFlags = []string{imap.SeenFlag} + ids, err := c.Client.Search(criteria) + if err != nil { + return nil, err + } + + return ids, err +} + +// Store store status +func (c *Client) Store(mailBox string, mID uint32, isAdd bool, flags []interface{}) error { + if err := c.Login(); err != nil { + return err + } + defer func() { + err := c.LogOut() + if err != nil { + log.Warn("Imap.Logout", err) + } + }() + + return c.store(mailBox, mID, isAdd, flags) +} + +// store store status without login +func (c *Client) store(mailBox string, mID uint32, isAdd bool, flags []interface{}) error { + if len(mailBox) == 0 { + mailBox = "INBOX" + } + + // Select INBOX + _, err := c.Client.Select(mailBox, false) + if err != nil { + return err + } + + seqSet := new(imap.SeqSet) + seqSet.AddNum(mID) + + var opt imap.FlagsOp + if isAdd { + opt = imap.AddFlags + } else { + opt = imap.RemoveFlags + } + + item := imap.FormatFlagsOp(opt, true) + + return c.Client.Store(seqSet, item, flags, nil) +} + +// DeleteMail delete one mail +func (c *Client) DeleteMail(mailBox string, mID uint32) error { + if err := c.Login(); err != nil { + return err + } + defer func() { + err := c.LogOut() + if err != nil { + log.Warn("Imap.Logout", err) + } + }() + + // First mark the message as deleted + if err := c.store(mailBox, mID, true, []interface{}{imap.DeletedFlag}); err != nil { + return err + } + + // Then delete it + err := c.Client.Expunge(nil) + return err +} + +// FetchMail fetch a mail +func (c *Client) FetchMail(id uint32, box string, requestBody bool) (*mail.Reader, error) { + var err error + + if err = c.Login(); err != nil { + return nil, err + } + defer func() { + err := c.LogOut() + if err != nil { + log.Warn("Imap.Logout", err) + } + }() + + if len(box) == 0 { + box = "INBOX" + } + + // Select mail box + _, err = c.Client.Select(box, false) + if err != nil { + return nil, err + } + + seqSet := new(imap.SeqSet) + seqSet.AddNum(id) + + var section = imap.BodySectionName{} + if !requestBody { + section.BodyPartName.Specifier = imap.HeaderSpecifier + } + items := []imap.FetchItem{section.FetchItem()} + + messages := make(chan *imap.Message, 1) + + go func() { + err = c.Client.Fetch(seqSet, items, messages) + }() + + msg := <-messages + if err != nil { + return nil, err + } + if msg == nil { + return nil, errors.New("Server didn't returned message") + } + + r := msg.GetBody(§ion) + if r == nil { + return nil, errors.New("Server didn't returned message body") + } + + // Create a new mail reader + mr, err := mail.CreateReader(r) + if err != nil { + return nil, err + } + + return mr, nil +} + +// Mail save an mail data +type Mail struct { + Client *Client + ID uint32 + Box string + + // header + Date time.Time + Heads map[string][]*mail.Address + + // body + Content *goquery.Document + + Deleted bool +} + +// GetUnReadMails get all unread mails +func (c *Client) GetUnReadMails(mailBox string, limit int) ([]*Mail, error) { + ids, err := c.GetUnReadMailIDs(mailBox) + if err != nil { + return nil, err + } + + last := len(ids) + if last > limit { + last = limit + } + + mails := make([]*Mail, last) + for index, id := range ids[0:last] { + mails[index] = &Mail{ + ID: id, + Client: c, + Box: mailBox, + } + } + + return mails, nil +} + +// LoadHeader load Head data +func (m *Mail) LoadHeader(requestHeads []string) error { + mr, err := m.Client.FetchMail(m.ID, m.Box, false) + if err != nil { + return err + } + defer mr.Close() + + m.Date, err = mr.Header.Date() + if err != nil { + return err + } + + if m.Heads == nil { + m.Heads = make(map[string][]*mail.Address) + } + + var v []*mail.Address + for _, head := range requestHeads { + if v, err = mr.Header.AddressList(head); err != nil { + return err + } + m.Heads[head] = v + } + + return nil +} + +// LoadBody load body data +func (m *Mail) LoadBody() error { + mr, err := m.Client.FetchMail(m.ID, m.Box, true) + if err != nil { + return err + } + // defer mr.Close() + + for { + p, err := mr.NextPart() + if err == io.EOF { + break + } else if err != nil { + return err + } + + switch p.Header.(type) { + case *mail.InlineHeader: + + m.Content, err = goquery.NewDocumentFromReader(p.Body) + return err + + case *mail.AttachmentHeader: + // TODO: how to handle attachment + // This is an attachment + // filename, err := h.Filename() + // if err != nil { + + // } + } + } + + return nil +} + +// SetRead set read status +func (m *Mail) SetRead(isRead bool) error { + return m.Client.Store(m.Box, m.ID, isRead, []interface{}{imap.SeenFlag}) +} + +// Delete delet this mail +func (m *Mail) Delete() error { + if m.Deleted { + return nil + } + err := m.Client.DeleteMail(m.Box, m.ID) + if err != nil { + return err + } + m.Deleted = true + + return nil +} diff --git a/services/imap/mail_reciver.go b/services/imap/mail_reciver.go new file mode 100644 index 0000000000000..f0141c2b64846 --- /dev/null +++ b/services/imap/mail_reciver.go @@ -0,0 +1,162 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package imap + +import ( + "fmt" + "net/url" + "strconv" + "strings" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/queue" + "code.gitea.io/gitea/modules/setting" + comment_service "code.gitea.io/gitea/services/comments" +) + +// mail read query +var mailReadQueue queue.Queue + +// NewContext start received mail read queue service +func NewContext() { + if setting.MailReciveService == nil || mailReadQueue != nil { + return + } + + mailReadQueue = queue.CreateQueue("mail_recive", func(data ...queue.Data) { + for _, datum := range data { + mail := datum.(*Mail) + if err := mail.LoadHeader([]string{"From", "To"}); err != nil { + log.Error("fetch mail header failed: %v", err) + } + + if len(mail.Heads["To"]) == 0 || + len(mail.Heads["From"]) == 0 { + continue + } + + if mail.Heads["To"][0].Address != setting.MailReciveService.ReciveEmail { + continue + } + + log.Trace("start read email from %v", mail.Heads["From"][0].String()) + if err := handleReciveEmail(mail); err != nil { + log.Error("handleReciveEmail(): %v", err) + } + log.Trace("finished read email from %v", mail.Heads["From"][0].String()) + } + }, &Mail{}) +} + +func handleReciveEmail(m *Mail) error { + if err := m.LoadBody(); err != nil { + return fmt.Errorf("m.LoadBody(): %v", err) + } + + from := m.Heads["From"][0].Address + doer, err := models.GetUserByEmail(from) + if err != nil { + if models.IsErrUserNotExist(err) { + return nil + } + return fmt.Errorf("models.GetUserByEmail(%v): %v", from, err) + } + + // chek if it's a reply mail to an issue or pull request + linkNode := m.Content.Find("a.reply-to") + if linkNode.Length() != 1 { + return nil + } + + linkHerf, has := linkNode.First().Attr("href") + if !has || len(linkHerf) == 0 { + return nil + } + + // expected link {{AppFullUrl}}/{{Owner}}/{{ReopName}}/{{issues/pulls}}/{{index}}#issuecomment-id + link, err := url.Parse(linkHerf) + if err != nil { + return fmt.Errorf("url.Parse(%v): %v", linkHerf, err) + } + + splitLink := strings.SplitN(link.Path[1:], "/", 4) + if len(splitLink) != 4 || + (splitLink[2] != "pulls" && splitLink[2] != "issues") { + return nil + } + + repoOwner := splitLink[0] + repoName := splitLink[1] + issueIndex, err := strconv.ParseInt(splitLink[3], 0, 64) + if err != nil { + return nil + } + if issueIndex <= 0 { + return nil + } + + repo, err := models.GetRepositoryByOwnerAndName(repoOwner, repoName) + if err != nil { + if models.IsErrRepoNotExist(err) { + return nil + } + + return fmt.Errorf("models.GetRepositoryByOwnerAndName(%v,%v): %v", repoOwner, repoName, err) + } + + if repo.IsArchived { + return nil + } + + perm, err := models.GetUserRepoPermission(repo, doer) + if err != nil { + return fmt.Errorf("models.GetUserRepoPermission(): %v", err) + } + + issue, err := models.GetIssueWithAttrsByIndex(repo.ID, issueIndex) + if err != nil { + if models.IsErrIssueNotExist(err) { + return nil + } + + return fmt.Errorf("models.GetIssueWithAttrsByIndex(%v,%v): %v", repo.ID, issueIndex, err) + } + + // check permission + permUnit := models.UnitTypeIssues + if issue.IsPull { + permUnit = models.UnitTypePullRequests + } + + if issue.IsLocked && !perm.CanWrite(permUnit) { + return nil + } + + if !issue.IsLocked && !perm.CanRead(permUnit) { + return nil + } + + comment, err := m.Content.Html() + if err != nil { + return fmt.Errorf("m.Content.Html(): %v", err) + } + + _, err = comment_service.CreateIssueComment(doer, + repo, + issue, + comment, nil) + if err != nil { + return fmt.Errorf("comment_service.CreateIssueComment(): %v", err) + } + + _ = m.SetRead(true) + + if setting.MailReciveService.DeleteRodeMail { + _ = m.Delete() + } + + return nil +} diff --git a/services/mailer/mail.go b/services/mailer/mail.go index b4217c046612b..dae8cea40af2f 100644 --- a/services/mailer/mail.go +++ b/services/mailer/mail.go @@ -251,6 +251,11 @@ func composeIssueCommentMessages(ctx *mailCommentContext, tos []string, fromMent msg := NewMessageFrom([]string{to}, ctx.Doer.DisplayName(), setting.MailService.FromEmail, subject, mailBody.String()) msg.Info = fmt.Sprintf("Subject: %s, %s", subject, info) + if setting.MailReciveService != nil && + len(setting.MailReciveService.ReciveEmail) > 0 { + msg.SetHeader("Reply-To", "<"+setting.MailReciveService.ReciveEmail+">") + } + // Set Message-ID on first message so replies know what to reference if actName == "new" { msg.SetHeader("Message-ID", "<"+ctx.Issue.ReplyReference()+">") diff --git a/templates/mail/issue/default.tmpl b/templates/mail/issue/default.tmpl index e062dca7f1b5d..d3ce6b4afae29 100644 --- a/templates/mail/issue/default.tmpl +++ b/templates/mail/issue/default.tmpl @@ -83,7 +83,7 @@
---
- View it on {{AppName}}.
+ .