diff --git a/README.md b/README.md index 56a5ee8..41ab7dd 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ With chatz, you can streamline your notification processes across multiple platf - Telegram: [Read documentation](docs/telegram.md) - Discord: [Read documentation](docs/discord.md) - Redis: [Read documentation](docs/redis.md) +- SMTP: [Read documentation](docs/smtp.md) ## Installation Download and install executable binary from GitHub releases page. @@ -113,6 +114,47 @@ CONNECTION_URL= CHANNEL_ID= ``` +- Config for smtp provider with TLS +```ini +[default] +PROVIDER=smtp +SMTP_HOST=smtp.gmail.com +SMTP_PORT=465 +SMTP_USE_TLS=true +SMTP_USER= +SMTP_PASSWORD= +SMTP_SUBJECT= +SMTP_FROM= +SMTP_TO= +``` + +- Config for smtp provider with STARTTLS +```ini +[default] +PROVIDER=smtp +SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 +SMTP_USE_STARTTLS=true +SMTP_USER= +SMTP_PASSWORD= +SMTP_SUBJECT= +SMTP_FROM= +SMTP_TO= +``` + +- Config for smtp provider without encryption +```ini +[default] +PROVIDER=smtp +SMTP_HOST=smtp.yourmailserver.com +SMTP_PORT=25 +SMTP_USER= +SMTP_PASSWORD= +SMTP_SUBJECT= +SMTP_FROM= +SMTP_TO= +``` + ### System Environment Support Chatz also support system environment variable. To use system environment variable then use `--from-env` or `-e` flag. Name of the environment variable is same as `chatz.ini` config key, for example `export PROVIDER=slack`. diff --git a/config/config.go b/config/config.go index 98ecb83..7cef296 100644 --- a/config/config.go +++ b/config/config.go @@ -2,10 +2,19 @@ package config // Environment type Config struct { - Provider string - WebHookURL string - Token string - ChannelId string - ChatId string - ConnectionURL string + Provider string + WebHookURL string + Token string + ChannelId string + ChatId string + ConnectionURL string + SMTPHost string + SMTPPort string + UseTLS bool + UseSTARTTLS bool + SMTPUser string + SMTPPassword string + SMTPSubject string + SMTPFrom string + SMTPTo string } diff --git a/constants/common.go b/constants/common.go index 22027dc..fff8268 100644 --- a/constants/common.go +++ b/constants/common.go @@ -1,9 +1,10 @@ package constants const ( - PROVIDER_SLACK="slack" - PROVIDER_DISCORD="discord" - PROVIDER_TELEGRAM="telegram" - PROVIDER_GOOGLE="google" - PROVIDER_REDIS="redis" + PROVIDER_SLACK = "slack" + PROVIDER_DISCORD = "discord" + PROVIDER_TELEGRAM = "telegram" + PROVIDER_GOOGLE = "google" + PROVIDER_REDIS = "redis" + PROVIDER_SMTP = "smtp" ) diff --git a/docs/smtp.md b/docs/smtp.md new file mode 100644 index 0000000..8998533 --- /dev/null +++ b/docs/smtp.md @@ -0,0 +1,59 @@ +# SMTP with Gmail + +This document explains how to configure SMTP with Gmail, including creating an App Password, and understanding TLS/STARTTLS and encryption options. + + +## 1. Creating an App Password + +Gmail requires an "App Password" for less secure apps accessing your account. This is a more secure alternative to using your regular Gmail password directly. + +1. Go to your [Google account security settings](https://myaccount.google.com/security). +2. Scroll down to "Signing in to Google" and click "App Passwords". +3. Select "Mail" as the app and "Other (Custom name)" as the device. Give it a descriptive name (e.g., "My SMTP App"). +4. Click "Generate". A new password will be displayed. **Copy this password immediately; you won't be able to see it again.** + +## 2. SMTP Server Settings + +* **Server:** `smtp.gmail.com` +* **Port:** + * **STARTTLS:** 587 (Recommended) + * **TLS:** 465 + * **No Encryption (Insecure - Avoid):** 25 +* **Username:** Your full Gmail address (e.g., `yourname@gmail.com`) +* **Password:** The App Password you generated. + + +## 3. TLS/STARTTLS and Encryption + +* **TLS (Transport Layer Security) / STARTTLS:** These are encryption protocols that secure the connection between your email client and the SMTP server. They encrypt your email messages and prevent eavesdropping. **Using TLS/STARTTLS is strongly recommended.** Many email clients support STARTTLS, initiating encryption during the connection process. + + +* **No Encryption:** Sending emails without encryption is highly discouraged. Your email, including the subject, body, and any attachments, could be intercepted by malicious actors. Avoid this unless absolutely necessary, and only with trusted parties. + + + +## 4. Example Configuration (Illustrative - adapt to your email client) + + +This is a generic example; the exact settings will depend on your email client (e.g., Outlook, Thunderbird). Refer to your email client's documentation for specific instructions. + + +``` +SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 +SMTP_USE_STARTTLS=true +``` + +Remember to replace placeholders with your actual information. + +## 5. Troubleshooting + + +If you encounter issues, check: + +* **Correct App Password:** Verify you copied the correct App Password. +* **Firewall Settings:** Ensure your firewall isn't blocking outgoing connections on port 587 (or 465 if using no encryption). +* **Email Client Configuration:** Double-check all server settings in your email client. + +Using an App Password is crucial for securing your Gmail account when using SMTP. Always prioritize using TLS/STARTTLS encryption for the security of your emails. +``` diff --git a/main.go b/main.go index 77b33bb..bc06edb 100644 --- a/main.go +++ b/main.go @@ -10,106 +10,104 @@ import ( ) var ( - AppVersion = "v0.0.1" - CommitHash = "unknown" - BuildDate = "unknown" + AppVersion = "v0.0.1" + CommitHash = "unknown" + BuildDate = "unknown" ) func main() { - - var version bool - var profile string - var threadId string - var output bool - var fromEnv bool - app := cli.NewApp() - app.Name = "chatz" - app.Description = "chatz is a versatile messaging app designed to send notifications to Google Chat, Slack, Discord, Telegram and Redis." - app.Flags = []cli.Flag { - &cli.BoolFlag{ - Name: "output", - Aliases: []string{"o"}, - Usage: "Print output to stdout", - Destination: &output, - }, - &cli.StringFlag{ - Name: "profile", - Aliases: []string{"p"}, - Value: "default", - Usage: "Profile from .chatz.ini", - Destination: &profile, - }, - &cli.StringFlag{ - Name: "thread-id", - Aliases: []string{"t"}, - Value: "", - Usage: "Thread ID for reply to a message", - Destination: &threadId, - }, - &cli.BoolFlag{ - Name: "version", - Aliases: []string{"v"}, - Usage: "Print the version number", - Destination: &version, - }, - &cli.BoolFlag{ - Name: "from-env", - Aliases: []string{"e"}, - Usage: "To use config from environment variables", - Destination: &fromEnv, - }, - } - app.Action = func(ctx *cli.Context) error { - if version { - fmt.Println("chatz version: ", AppVersion) - fmt.Println("Commit Hash: ", CommitHash) - fmt.Println("Build Date: ", BuildDate) - return nil - } + var version bool + var profile string + var threadId string + var output bool + var fromEnv bool + app := cli.NewApp() + app.Name = "chatz" + app.Description = "chatz is a versatile messaging app designed to send notifications to Google Chat, Slack, Discord, Telegram and Redis." + app.Flags = []cli.Flag{ + &cli.BoolFlag{ + Name: "output", + Aliases: []string{"o"}, + Usage: "Print output to stdout", + Destination: &output, + }, + &cli.StringFlag{ + Name: "profile", + Aliases: []string{"p"}, + Value: "default", + Usage: "Profile from .chatz.ini", + Destination: &profile, + }, + &cli.StringFlag{ + Name: "thread-id", + Aliases: []string{"t"}, + Value: "", + Usage: "Thread ID for reply to a message", + Destination: &threadId, + }, + &cli.BoolFlag{ + Name: "version", + Aliases: []string{"v"}, + Usage: "Print the version number", + Destination: &version, + }, + &cli.BoolFlag{ + Name: "from-env", + Aliases: []string{"e"}, + Usage: "To use config from environment variables", + Destination: &fromEnv, + }, + } + app.Action = func(ctx *cli.Context) error { + if version { + fmt.Println("chatz version: ", AppVersion) + fmt.Println("Commit Hash: ", CommitHash) + fmt.Println("Build Date: ", BuildDate) + return nil + } + + var message string + if ctx.Args().Len() == 0 { + fmt.Println("Please provide a message.") + return nil + } + for i, a := range ctx.Args().Slice() { + if i == 0 { + message = a + continue + } + message = fmt.Sprintf("%s %s", message, a) + } - var message string - if ctx.Args().Len() == 0 { - fmt.Println("Please provide a message.") - return nil - } - for i, a := range ctx.Args().Slice() { - if i == 0 { - message = a - continue - } - message = fmt.Sprintf("%s %s",message, a) - } - - env, err := utils.LoadEnv(profile, fromEnv) - if err!=nil { - return nil - } - provider, err := providers.NewProvider(env) - if err!=nil { - fmt.Println(err.Error()) - return nil - } + env, err := utils.LoadEnv(profile, fromEnv) + if err != nil { + return nil + } - if len(threadId) > 0 { - res, _ := provider.Reply(threadId, message) - if output { - fmt.Println(res) - } - return nil - } - res, _ := provider.Post(message) - if output { - fmt.Println(res) - } + provider, err := providers.NewProvider(env) + if err != nil { + fmt.Println(err.Error()) + return nil + } - return nil - } + if len(threadId) > 0 { + res, _ := provider.Reply(threadId, message) + if output { + fmt.Println(res) + } + return nil + } + res, err := provider.Post(message) + if output { + fmt.Println(res) + } + return nil + } - if err := app.Run(os.Args); err != nil { + if err := app.Run(os.Args); err != nil { panic(err) } } - diff --git a/man/chatz.1 b/man/chatz.1 index 3fe5b4f..4b0177b 100644 --- a/man/chatz.1 +++ b/man/chatz.1 @@ -115,6 +115,22 @@ CONNECTION_URL= CHANNEL_ID= .fi +.TP +Configuration for SMTP. + +.nf +[default] +PROVIDER=smtp +SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 +SMTP_USE_STARTTLS=true +SMTP_USER= +SMTP_PASSWORD= +SMTP_SUBJECT= +SMTP_FROM= +SMTP_TO= +.fi + .TP Ensure that each service is properly configured with valid tokens and webhook URLs. diff --git a/providers/agent.go b/providers/agent.go index 7c9d755..b6b4ea5 100644 --- a/providers/agent.go +++ b/providers/agent.go @@ -8,24 +8,26 @@ import ( ) type Provider interface { - Post(message string) (interface{}, error) - Reply(threadId string, message string) (interface{}, error) + Post(message string) (interface{}, error) + Reply(threadId string, message string) (interface{}, error) } func NewProvider(env *config.Config) (Provider, error) { - switch env.Provider { - case constants.PROVIDER_SLACK: - return &SlackProvider{config: env}, nil - case constants.PROVIDER_GOOGLE: - return &GoogleProvider{config: env}, nil - case constants.PROVIDER_TELEGRAM: - return &TelegramProvider{config: env}, nil - case constants.PROVIDER_DISCORD: - return &DiscordProvider{config: env}, nil - case constants.PROVIDER_REDIS: - return &RedisProvider{config: env}, nil - default: - return nil, errors.New("Invalid provider config in ~/.chatz.ini") - } + switch env.Provider { + case constants.PROVIDER_SLACK: + return &SlackProvider{config: env}, nil + case constants.PROVIDER_GOOGLE: + return &GoogleProvider{config: env}, nil + case constants.PROVIDER_TELEGRAM: + return &TelegramProvider{config: env}, nil + case constants.PROVIDER_DISCORD: + return &DiscordProvider{config: env}, nil + case constants.PROVIDER_REDIS: + return &RedisProvider{config: env}, nil + case constants.PROVIDER_SMTP: + return &SMTPProvider{config: env}, nil + default: + return nil, errors.New("Invalid provider config in ~/.chatz.ini") + } } diff --git a/providers/smtp.go b/providers/smtp.go new file mode 100644 index 0000000..d5bed09 --- /dev/null +++ b/providers/smtp.go @@ -0,0 +1,142 @@ +package providers + +import ( + "crypto/tls" + "errors" + "fmt" + "net" + "net/smtp" + "strings" + + "github.com/tech-thinker/chatz/config" +) + +type SMTPProvider struct { + config *config.Config +} + +func (agent *SMTPProvider) Post(message string) (interface{}, error) { + host := agent.config.SMTPHost + port := agent.config.SMTPPort + smtpServer := fmt.Sprintf("%s:%s", host, port) + user := agent.config.SMTPUser + password := agent.config.SMTPPassword + + subject := agent.config.SMTPSubject + from := agent.config.SMTPFrom + recipients := agent.config.SMTPTo + + if len(from) == 0 { + from = user + } + + if len(subject) == 0 { + subject = "Chatz Notification" + } + + // Create the message with proper headers + msg := []byte(fmt.Sprintf( + "From: %s\r\nTo: %s\r\nSubject: %s\r\n\r\n%s", + from, recipients, subject, message, + )) + + // Split recipients into a slice + to := strings.Split(recipients, ",") + + // Handle UseTLS + if agent.config.UseTLS { + tlsConfig := &tls.Config{ + ServerName: host, + } + + // Establish a secure connection using TLS + conn, err := tls.Dial("tcp", smtpServer, tlsConfig) + if err != nil { + return nil, fmt.Errorf("failed to connect to SMTP server using TLS: %w", err) + } + defer conn.Close() + + client, err := smtp.NewClient(conn, host) + if err != nil { + return nil, fmt.Errorf("failed to create SMTP client over TLS: %w", err) + } + defer client.Quit() + + // Authenticate + auth := smtp.PlainAuth("", user, password, host) + if err = client.Auth(auth); err != nil { + return nil, fmt.Errorf("failed to authenticate: %w", err) + } + + // Send the email + return sendEmail(client, from, to, msg) + } + + // Connect without encryption initially + conn, err := net.Dial("tcp", smtpServer) + if err != nil { + return nil, fmt.Errorf("failed to connect to SMTP server: %w", err) + } + defer conn.Close() + + client, err := smtp.NewClient(conn, host) + if err != nil { + return nil, fmt.Errorf("failed to create SMTP client: %w", err) + } + defer client.Quit() + + // Handle UseSTARTTLS + if agent.config.UseSTARTTLS { + tlsConfig := &tls.Config{ + ServerName: host, + } + + // Upgrade to STARTTLS + if err = client.StartTLS(tlsConfig); err != nil { + return nil, fmt.Errorf("failed to start TLS: %w", err) + } + } + + // Authenticate + auth := smtp.PlainAuth("", user, password, host) + if err = client.Auth(auth); err != nil { + return nil, fmt.Errorf("failed to authenticate: %w", err) + } + + // Send the email + return sendEmail(client, from, to, msg) +} + +// Helper function to send email +func sendEmail(client *smtp.Client, from string, to []string, msg []byte) (interface{}, error) { + // Set the sender and recipients + if err := client.Mail(from); err != nil { + return nil, fmt.Errorf("failed to set sender: %w", err) + } + for _, recipient := range to { + if err := client.Rcpt(recipient); err != nil { + return nil, fmt.Errorf("failed to add recipient %s: %w", recipient, err) + } + } + + // Write the email body + writer, err := client.Data() + if err != nil { + return nil, fmt.Errorf("failed to open data writer: %w", err) + } + _, err = writer.Write(msg) + if err != nil { + return nil, fmt.Errorf("failed to write email body: %w", err) + } + err = writer.Close() + if err != nil { + return nil, fmt.Errorf("failed to close data writer: %w", err) + } + + return `{"status": "success"}`, nil +} + +func (agent *SMTPProvider) Reply(threadId string, message string) (interface{}, error) { + fmt.Println("Reply to SMTP not supported yet.") + return nil, errors.New("Reply to SMTP not supported yet.") +} diff --git a/utils/common.go b/utils/common.go index bd92cb0..8dbfb5d 100644 --- a/utils/common.go +++ b/utils/common.go @@ -9,78 +9,112 @@ import ( "github.com/tech-thinker/chatz/config" ) - func LoadEnv(profile string, fromEnv bool) (*config.Config, error) { - if fromEnv { - return loadEnvFromSystemEnv() - } else { - return loadEnvFromFile(profile) - } + if fromEnv { + return loadEnvFromSystemEnv() + } else { + return loadEnvFromFile(profile) + } } func loadEnvFromSystemEnv() (*config.Config, error) { - v := viper.New() - v.AutomaticEnv() - - // Get values from system environment - provider := v.GetString("PROVIDER") - token := v.GetString("TOKEN") - channelId := v.GetString("CHANNEL_ID") - webHookURL := v.GetString("WEB_HOOK_URL") - chatId := v.GetString("CHAT_ID") - connectionURL := v.GetString("CONNECTION_URL") - - var env config.Config - - env.Provider = provider - env.WebHookURL = webHookURL - env.Token = token - env.ChannelId = channelId - env.ChatId = chatId - env.ConnectionURL = connectionURL - - return &env, nil + v := viper.New() + v.AutomaticEnv() + + // Get values from system environment + provider := v.GetString("PROVIDER") + token := v.GetString("TOKEN") + channelId := v.GetString("CHANNEL_ID") + webHookURL := v.GetString("WEB_HOOK_URL") + chatId := v.GetString("CHAT_ID") + connectionURL := v.GetString("CONNECTION_URL") + smtpHost := v.GetString("SMTP_HOST") + smtpPort := v.GetString("SMTP_PORT") + useTLS := v.GetBool("SMTP_USE_TLS") + useSTARTTLS := v.GetBool("SMTP_USE_STARTTLS") + smtpUser := v.GetString("SMTP_USER") + smtpPassword := v.GetString("SMTP_PASSWORD") + smtpSubject := v.GetString("SMTP_SUBJECT") + smtpFrom := v.GetString("SMTP_FROM") + smtpTo := v.GetString("SMTP_TO") + + var env config.Config + + env.Provider = provider + env.WebHookURL = webHookURL + env.Token = token + env.ChannelId = channelId + env.ChatId = chatId + env.ConnectionURL = connectionURL + env.SMTPHost = smtpHost + env.SMTPPort = smtpPort + env.UseTLS = useTLS + env.UseSTARTTLS = useSTARTTLS + env.SMTPUser = smtpUser + env.SMTPPassword = smtpPassword + env.SMTPSubject = smtpSubject + env.SMTPFrom = smtpFrom + env.SMTPTo = smtpTo + + return &env, nil } func loadEnvFromFile(profile string) (*config.Config, error) { - // Get the home directory of the user - homeDir, err := os.UserHomeDir() - if err != nil { - fmt.Printf("Error getting home directory: %s\n", err) - return nil, err - } - - // Set the configuration file path - configPath := filepath.Join(homeDir, ".chatz.ini") - - // Set the file name and type - viper.SetConfigFile(configPath) // full path to the config file - viper.SetConfigType("ini") // or "yaml", "json", etc. - - // Read the configuration file - err = viper.ReadInConfig() - if err != nil { - fmt.Printf("Error reading config file: %s\n", err) - return nil, err - } - - // Get values from the INI file - provider := viper.GetString(fmt.Sprintf("%s.PROVIDER", profile)) - token := viper.GetString(fmt.Sprintf("%s.TOKEN", profile)) - channelId := viper.GetString(fmt.Sprintf("%s.CHANNEL_ID", profile)) - webHookURL := viper.GetString(fmt.Sprintf("%s.WEB_HOOK_URL", profile)) - chatId := viper.GetString(fmt.Sprintf("%s.CHAT_ID", profile)) - connectionURL := viper.GetString(fmt.Sprintf("%s.CONNECTION_URL", profile)) - - - var env config.Config - env.Provider = provider - env.WebHookURL = webHookURL - env.Token = token - env.ChannelId = channelId - env.ChatId = chatId - env.ConnectionURL = connectionURL - - return &env, nil + // Get the home directory of the user + homeDir, err := os.UserHomeDir() + if err != nil { + fmt.Printf("Error getting home directory: %s\n", err) + return nil, err + } + + // Set the configuration file path + configPath := filepath.Join(homeDir, ".chatz.ini") + + // Set the file name and type + viper.SetConfigFile(configPath) // full path to the config file + viper.SetConfigType("ini") // or "yaml", "json", etc. + + // Read the configuration file + err = viper.ReadInConfig() + if err != nil { + fmt.Printf("Error reading config file: %s\n", err) + return nil, err + } + + // Get values from the INI file + provider := viper.GetString(fmt.Sprintf("%s.PROVIDER", profile)) + token := viper.GetString(fmt.Sprintf("%s.TOKEN", profile)) + channelId := viper.GetString(fmt.Sprintf("%s.CHANNEL_ID", profile)) + webHookURL := viper.GetString(fmt.Sprintf("%s.WEB_HOOK_URL", profile)) + chatId := viper.GetString(fmt.Sprintf("%s.CHAT_ID", profile)) + connectionURL := viper.GetString(fmt.Sprintf("%s.CONNECTION_URL", profile)) + + smtpHost := viper.GetString(fmt.Sprintf("%s.SMTP_HOST", profile)) + smtpPort := viper.GetString(fmt.Sprintf("%s.SMTP_PORT", profile)) + useTLS := viper.GetBool(fmt.Sprintf("%s.SMTP_USE_TLS", profile)) + useSTARTTLS := viper.GetBool(fmt.Sprintf("%s.SMTP_USE_STARTTLS", profile)) + smtpUser := viper.GetString(fmt.Sprintf("%s.SMTP_USER", profile)) + smtpPassword := viper.GetString(fmt.Sprintf("%s.SMTP_PASSWORD", profile)) + smtpSubject := viper.GetString(fmt.Sprintf("%s.SMTP_SUBJECT", profile)) + smtpFrom := viper.GetString(fmt.Sprintf("%s.SMTP_FROM", profile)) + smtpTo := viper.GetString(fmt.Sprintf("%s.SMTP_TO", profile)) + + var env config.Config + env.Provider = provider + env.WebHookURL = webHookURL + env.Token = token + env.ChannelId = channelId + env.ChatId = chatId + env.ConnectionURL = connectionURL + env.SMTPHost = smtpHost + env.SMTPPort = smtpPort + env.UseTLS = useTLS + env.UseSTARTTLS = useSTARTTLS + env.SMTPUser = smtpUser + env.SMTPPassword = smtpPassword + env.SMTPSubject = smtpSubject + env.SMTPFrom = smtpFrom + env.SMTPTo = smtpTo + + return &env, nil } -