You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
224 lines
6.1 KiB
224 lines
6.1 KiB
1 year ago
|
/*
|
||
|
massmailer - send an e-mail to a list of addresses
|
||
|
Copyright (C) 2023 Kasyanov Nikolay Alexeyevich (Unbewohnte)
|
||
|
|
||
|
This program is free software: you can redistribute it and/or modify
|
||
|
it under the terms of the GNU Affero General Public License as
|
||
|
published by the Free Software Foundation, either version 3 of the
|
||
|
License, or (at your option) any later version.
|
||
|
|
||
|
This program is distributed in the hope that it will be useful,
|
||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
GNU Affero General Public License for more details.
|
||
|
|
||
|
You should have received a copy of the GNU Affero General Public License
|
||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||
|
*/
|
||
|
|
||
|
package main
|
||
|
|
||
|
import (
|
||
|
"Unbewohnte/massmail/config"
|
||
|
"Unbewohnte/massmail/logger"
|
||
|
"bufio"
|
||
|
"flag"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"mime"
|
||
|
"os"
|
||
|
"path/filepath"
|
||
|
"strings"
|
||
|
"sync"
|
||
|
"time"
|
||
|
|
||
|
"gopkg.in/gomail.v2"
|
||
|
)
|
||
|
|
||
|
const version = "v0.1.2"
|
||
|
|
||
|
const (
|
||
|
configFilename string = "conf.json"
|
||
|
)
|
||
|
|
||
|
var (
|
||
|
printVersion = flag.Bool(
|
||
|
"version", false,
|
||
|
"Print version and exit",
|
||
|
)
|
||
|
|
||
|
wDir = flag.String(
|
||
|
"wdir", "",
|
||
|
"Force set working directory",
|
||
|
)
|
||
|
|
||
|
configFile = flag.String(
|
||
|
"conf", configFilename,
|
||
|
"Configuration file name to create|look for",
|
||
|
)
|
||
|
|
||
|
workingDirectory string
|
||
|
configFilePath string
|
||
|
)
|
||
|
|
||
|
func init() {
|
||
|
// set log output
|
||
|
logger.SetOutput(os.Stdout)
|
||
|
|
||
|
// parse and process flags
|
||
|
flag.Parse()
|
||
|
|
||
|
if *printVersion {
|
||
|
fmt.Printf(
|
||
|
"massmailer %s\n(c) 2023 Kasyanov Nikolay Alexeyevich (Unbewohnte)\n",
|
||
|
version,
|
||
|
)
|
||
|
os.Exit(0)
|
||
|
}
|
||
|
|
||
|
// print logo
|
||
|
logger.GetOutput().Write([]byte(
|
||
|
`███╗ ███╗ █████╗ ███████╗███████╗███╗ ███╗ █████╗ ██╗██╗
|
||
|
████╗ ████║██╔══██╗██╔════╝██╔════╝████╗ ████║██╔══██╗██║██║
|
||
|
██╔████╔██║███████║███████╗███████╗██╔████╔██║███████║██║██║
|
||
|
██║╚██╔╝██║██╔══██║╚════██║╚════██║██║╚██╔╝██║██╔══██║██║██║
|
||
|
██║ ╚═╝ ██║██║ ██║███████║███████║██║ ╚═╝ ██║██║ ██║██║███████╗
|
||
|
╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚══════╝ `,
|
||
|
))
|
||
|
logger.GetOutput().Write([]byte(version + " by Unbewohnte\n\n"))
|
||
|
|
||
|
// work out working directory path
|
||
|
if *wDir != "" {
|
||
|
workingDirectory = *wDir
|
||
|
} else {
|
||
|
wdir, err := os.Getwd()
|
||
|
if err != nil {
|
||
|
logger.Error("Failed to determine working directory path: %s", err)
|
||
|
return
|
||
|
}
|
||
|
workingDirectory = wdir
|
||
|
}
|
||
|
|
||
|
logger.Info("Working in \"%s\"", workingDirectory)
|
||
|
|
||
|
// global path to configuration file
|
||
|
configFilePath = filepath.Join(workingDirectory, *configFile)
|
||
|
}
|
||
|
|
||
|
func main() {
|
||
|
// open config
|
||
|
logger.Info("Trying to open config \"%s\"", configFilePath)
|
||
|
|
||
|
var conf *config.Conf
|
||
|
conf, err := config.OpenConfigFile(configFilePath)
|
||
|
if err != nil {
|
||
|
logger.Error(
|
||
|
"Failed to open configuration file: %s. Creating a new one with the same name instead...",
|
||
|
err,
|
||
|
)
|
||
|
|
||
|
err = config.CreateConfigFile(*config.Default(), configFilePath)
|
||
|
if err != nil {
|
||
|
logger.Error("Could not create new configuration file: %s", err)
|
||
|
return
|
||
|
}
|
||
|
logger.Info("Created new configuration file. Exiting...")
|
||
|
return
|
||
|
}
|
||
|
logger.Info("Successfully opened configuration file")
|
||
|
|
||
|
// Sanitize inputs
|
||
|
if len(conf.From) == 0 {
|
||
|
logger.Error("Configuration's from is not specified!")
|
||
|
}
|
||
|
|
||
|
if len(conf.Host) == 0 {
|
||
|
logger.Error("Configuration's host is not specified!")
|
||
|
}
|
||
|
|
||
|
if len(conf.FromHostPassword) == 0 {
|
||
|
logger.Error("Configuration's from_host_password is not specified!")
|
||
|
}
|
||
|
|
||
|
if len(conf.ToDBPath) == 0 {
|
||
|
logger.Error("Configuration's to_db_path is not specified!")
|
||
|
}
|
||
|
|
||
|
if len(conf.MessageFilePath) == 0 {
|
||
|
logger.Error("Configuration's message_file_path is not specified!")
|
||
|
}
|
||
|
|
||
|
// Retrieve email message
|
||
|
messageFile, err := os.Open(conf.MessageFilePath)
|
||
|
if err != nil {
|
||
|
logger.Error("Failed to open a message file: %s", err)
|
||
|
return
|
||
|
}
|
||
|
defer messageFile.Close()
|
||
|
|
||
|
isHtml := strings.HasSuffix(conf.MessageFilePath, ".html")
|
||
|
|
||
|
messageBytes, err := io.ReadAll(messageFile)
|
||
|
if err != nil {
|
||
|
logger.Error("Failed to read message contents: %s", err)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Work out email addresses file
|
||
|
addressesFile, err := os.Open(conf.ToDBPath)
|
||
|
if err != nil {
|
||
|
logger.Error("Failed to open addresses file: %s", err)
|
||
|
return
|
||
|
}
|
||
|
defer addressesFile.Close()
|
||
|
|
||
|
// Authenticate!
|
||
|
mailDialer := gomail.NewDialer(conf.Host, int(conf.HostSMTPPort), conf.From, conf.FromHostPassword)
|
||
|
|
||
|
// Send message to every address
|
||
|
logger.Info("Starting to send...")
|
||
|
wg := &sync.WaitGroup{}
|
||
|
scanner := bufio.NewScanner(addressesFile)
|
||
|
for scanner.Scan() {
|
||
|
to := scanner.Text()
|
||
|
if len(to) == 0 {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
wg.Add(1)
|
||
|
go func(wg *sync.WaitGroup) {
|
||
|
defer wg.Done()
|
||
|
|
||
|
msg := gomail.NewMessage()
|
||
|
msg.SetHeader("From", conf.From)
|
||
|
msg.SetHeader("To", to)
|
||
|
msg.SetHeader("Subject", conf.MessageSubject)
|
||
|
for _, attachmentPath := range conf.MessageAttachmentPaths {
|
||
|
// Check if exists
|
||
|
stats, err := os.Stat(attachmentPath)
|
||
|
if err != nil || stats.IsDir() {
|
||
|
logger.Warning("%s does not exist or it is a directory. Skipping it...", attachmentPath)
|
||
|
continue
|
||
|
}
|
||
|
msg.Attach(attachmentPath)
|
||
|
}
|
||
|
if isHtml {
|
||
|
msg.SetBody(mime.TypeByExtension(".html"), string(messageBytes))
|
||
|
} else {
|
||
|
msg.SetBody(mime.TypeByExtension(".txt"), string(messageBytes))
|
||
|
}
|
||
|
err = mailDialer.DialAndSend(msg)
|
||
|
if err != nil {
|
||
|
logger.Info("Failed --> %s: %s", to, err)
|
||
|
} else {
|
||
|
logger.Info("Sent --> %s", to)
|
||
|
}
|
||
|
}(wg)
|
||
|
|
||
|
time.Sleep(time.Millisecond * time.Duration(conf.MessageSendDelayMS))
|
||
|
}
|
||
|
|
||
|
wg.Wait()
|
||
|
logger.Info("Completed")
|
||
|
}
|