В виду того, что спам всё ещё не вышел из моды, а всякая малварь не перестаёт захватывать сайты с целью использования сервера в качестве “говномёта”, в некоторых датацентрах и в сети интернет провайдеров домашнего интернета встречается ограничение исходящих соединений на 25 порт. Конечно, можно использовать какой-нибудь внешний smtp по шифрованному 465, но в моём случае в этом было мало проку. Мне нужны были только отчёты и уведомления со всех виртуалок на сервере и гарантия их доставки. Немного подумав, я решил, что интереснее всего было бы получать из в мой любимый на сегодняшний день, мессенджер Telegram.
Погуглив по сабжу, я практически сразу наткнулся про проект smtp2tg.
smpt2tg написан на языке Go, так что для начала нужно установить его. Первым делом, я попытался сделать это через пакетный менеджер и таким образом убил лишние полчаса. Поняв, что так ничего не будет работать, я грохнул всё ранее установленное и пошёл наипростейшим путём.
wget https://golang.org/dl/go1.15.6.linux-amd64.tar.gz
tar -C /usr/local -xzf go1.15.6.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin
echo "export PATH=$PATH:/usr/local/go/bin" >> ~/.bashrc
Теперь, убедимся, что мы засунули язык в нужное место. :)
Добавляем к этому делу пакеты-зависимости.
go get github.com/veqryn/go-email/email
go get github.com/spf13/viper
go get gopkg.in/telegram-bot-api.v4
go get github.com/ircop/smtp2tg
Необходимо создать конфиг для программы
Можно расположить его в /etc/smtp2tg.toml
[bot]
token = "ключ телеграм бота"
[receivers]
"*" = "id чата/канала/группы для wildcard"
"somemail@ex.uz" = "id чата/канала/группы для конкретного адреса"
[smtp]
listen = "0.0.0.0:25"
name = "ex.uz"
[logging]
file = "/var/log/smtp2tg.log"
debug = false
Теперь можно запустить прогу
/root/go/bin/smtp2tg -c /etc/smtp2tg.toml &
Любая почта, попавшая в этот smtp сервер будет направлена в соответствии с конфигом на id соответствующий адресату, либо по правилу “ловить всё”.
Если вам нужен настоящий почтовый ящик, просто натравите MX запись вашего домена на сервер, с запущенным smtp2tg. Однако, моя задача заключалась в том, чтобы отлавливать всю почту, которая уходит с моего сервера в любом направлении. Так, как порт 25 для меня всё равно заблокирован на выход, я не раздумывая добавил на гипервизоре правило dnat.
iptables -t nat -I PREROUTING -p tcp -d 0.0.0.0/0 --dport 25 -j DNAT --to-destination IP_СЕРВЕРА:25
Теперь эта прога будет перехватывать вообще всё, независимо от того, какой MX отрезолвился на адресе получателя.
На этом можно было бы и закрыть консольку, но обнаружилась одна нервирующая фича. Дело в том, что при парсинге письма, smtp2tg ищет в нём заголовок, в котором указан тип содержимого. Так вот, к примеру, функция mail() в php ничего такого в письмо по умолчанию не кладёт, а по сей причине кое-что до нас может не долететь, ибо будет отфильтровано как “пустой конверт”.
Меня такой расклад совершенно не устроил, и я, абсолютно не зная языка Go, полез что-то то там исправлять. :) Результат моих стараний можно увидеть ниже. Это слегка допиленная версия программы, которая в добавок теперь ещё и показывать тему письма, а так же, адрес отправителя.
package main
import (
"os"
"strconv"
"strings"
"flag"
"bytes"
"log"
"net"
"gopkg.in/telegram-bot-api.v4"
"github.com/spf13/viper"
"github.com/veqryn/go-email/email"
"github.com/ircop/smtp2tg/smtpd"
)
var receivers map[string]string
var bot *tgbotapi.BotAPI
var debug bool
func main() {
configFilePath := flag.String("c", "./smtp2tg.toml", "Config file location")
//pidFilePath := flag.String("p", "/var/run/smtp2tg.pid", "Pid file location")
flag.Parse()
// Load & parse config
viper.SetConfigFile(*configFilePath)
err := viper.ReadInConfig()
if( err != nil ) {
log.Fatal(err.Error())
}
// Logging
logfile := viper.GetString("logging.file")
if( logfile == "" ) {
log.Println("No logging.file defined in config, outputting to stdout")
} else {
lf, err := os.OpenFile(logfile, os.O_APPEND | os.O_CREATE | os.O_RDWR, 0666)
if( err != nil ) {
log.Fatal(err.Error())
}
log.SetOutput(lf)
}
// Debug?
debug = viper.GetBool("logging.debug")
receivers = viper.GetStringMapString("receivers")
if( receivers["*"] == "" ) {
log.Fatal("No wildcard receiver (*) found in config.")
}
var token string = viper.GetString("bot.token")
if( token == "" ) {
log.Fatal("No bot.token defined in config")
}
var listen string = viper.GetString("smtp.listen")
var name string = viper.GetString("smtp.name")
if( listen == "" ) {
log.Fatal("No smtp.listen defined in config.")
}
if( name == "" ) {
log.Fatal("No smtp.name defined in config.")
}
// Initialize TG bot
bot, err = tgbotapi.NewBotAPI( token )
if( err != nil ) {
log.Fatal(err.Error())
}
log.Printf("Bot authorized as %s", bot.Self.UserName )
log.Printf("Initializing smtp server on %s...", listen)
// Initialize SMTP server
err_ := smtpd.ListenAndServe(listen, mailHandler, "mail2tg", "", debug)
if( err_ != nil ) {
log.Fatal(err_.Error())
}
}
func mailHandler(origin net.Addr, from string, to []string, data []byte) {
from = strings.Trim(from, " ")
to[0] = strings.Trim(to[0], " ")
to[0] = strings.Trim(to[0], "<")
to[0] = strings.Trim(to[0], ">")
msg, err := email.ParseMessage(bytes.NewReader(data))
if( err != nil ) {
log.Printf("[MAIL ERROR]: %s", err.Error())
return
}
subject := msg.Header.Get("Subject")
myBytes := msg.Body
log.Printf("Received mail from '%s' for '%s' with subject '%s'", from, to[0], subject)
// Find receivers and send to TG
var tgid string
if( receivers[to[0]] != "" ) {
tgid = receivers[to[0]]
} else {
tgid = receivers["*"]
}
textMsgs := msg.MessagesContentTypePrefix("text")
images := msg.MessagesContentTypePrefix("image")
if len(textMsgs) == 0 && len(images) == 0 {
if len(myBytes) == 0 {
log.Printf("mail doesn't contain text or image")
return
}
}
log.Printf("Relaying message to: %v", tgid)
i, err := strconv.ParseInt(tgid, 10, 64)
if( err != nil ) {
log.Printf("[ERROR]: wrong telegram id: not int64")
return
}
if len(textMsgs) > 0 {
bodyStr := "📬 from: "+from + " 💬 "+ subject + " 💬\r\n\r\n" + string(textMsgs[0].Body)
tgMsg := tgbotapi.NewMessage(i, bodyStr)
tgMsg.ParseMode = tgbotapi.ModeMarkdown
_, err = bot.Send(tgMsg)
if err != nil {
log.Printf("[ERROR]: telegram message send: '%s'", err.Error())
return
}
} else if len(myBytes) > 0 {
bodyStr := "📬 from: "+from + " 💬 "+ subject + " 💬\r\n\r\n" +string(myBytes)
tgMsg := tgbotapi.NewMessage(i, bodyStr)
tgMsg.ParseMode = tgbotapi.ModeMarkdown
_, err = bot.Send(tgMsg)
if err != nil {
log.Printf("[ERROR]: telegram message send: '%s'", err.Error())
return
}
}
// TODO Better to use 'sendMediaGroup' to send all attachments as a
// single message, but go telegram api has not implemented it yet
// https://github.com/go-telegram-bot-api/telegram-bot-api/issues/143
for _, part := range msg.MessagesContentTypePrefix("image") {
_, params, err := part.Header.ContentDisposition()
if err != nil {
log.Printf("[ERROR]: content disposition parse: '%s'", err.Error())
return
}
text := params["filename"]
tgFile := tgbotapi.FileBytes{Name: text, Bytes: part.Body}
tgMsg := tgbotapi.NewPhotoUpload(i, tgFile)
tgMsg.Caption = text
// It's not a separate message, so disable notification
tgMsg.DisableNotification = true
_, err = bot.Send(tgMsg)
if err != nil {
log.Printf("[ERROR]: telegram photo send: '%s'", err.Error())
return
}
}
}
Понятия не имею, почему изначально разработчик решил, что всё это не нужно, но в оригинальном коде предусмотрена доставка только тела письма.
Собирается этот код следующим образом:
mkdir ~/smtp2tg/
vi ~/smtp2tg/main.go
Вставляем вышеупомянутый код в main.go и запускаем “go build”
Запускаем, проверяем, получаем примерно такой результат:
Осталось только пихнуть в автозагрузку. (например в rc.local ?)
Наверное я допишу статью, когда найду нормальный способ демонизировать этот процесс под centos 7. А пока, отдаю на ваш суд, как говорится “as is”.