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.
223 lines
6.1 KiB
223 lines
6.1 KiB
/* |
|
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") |
|
}
|
|
|