Files
mail/main.go

338 lines
11 KiB
Go
Raw Normal View History

2023-06-13 23:31:19 -04:00
package main
import (
"errors"
2023-06-15 09:45:14 -04:00
"fmt"
2023-06-15 10:30:20 -04:00
"io"
2023-06-13 23:31:19 -04:00
"os"
2023-07-12 12:07:31 -04:00
"runtime/debug"
2023-07-31 10:32:02 -04:00
"strconv"
2023-06-15 10:59:25 -04:00
"strings"
2023-06-13 23:31:19 -04:00
tea "github.com/charmbracelet/bubbletea"
2023-07-13 11:05:27 -04:00
mcobra "github.com/muesli/mango-cobra"
"github.com/muesli/roff"
2023-06-15 10:30:20 -04:00
"github.com/resendlabs/resend-go"
2023-06-13 23:31:19 -04:00
"github.com/spf13/cobra"
)
2023-07-31 10:32:02 -04:00
// PopUnsafeHTML is the environment variable that enables unsafe HTML in the
// email body.
const PopUnsafeHTML = "POP_UNSAFE_HTML"
// ResendAPIKey is the environment variable that enables Resend as a delivery
// method and uses it to send the email.
const ResendAPIKey = "RESEND_API_KEY" //nolint:gosec
// PopFrom is the environment variable that sets the default "from" address.
const PopFrom = "POP_FROM"
// PopSignature is the environment variable that sets the default signature.
const PopSignature = "POP_SIGNATURE"
// PopSMTPHost is the host for the SMTP server if the user is using the SMTP delivery method.
const PopSMTPHost = "POP_SMTP_HOST"
// PopSMTPPort is the port for the SMTP server if the user is using the SMTP delivery method.
const PopSMTPPort = "POP_SMTP_PORT"
// PopSMTPUsername is the username for the SMTP server if the user is using the SMTP delivery method.
const PopSMTPUsername = "POP_SMTP_USERNAME"
// PopSMTPPassword is the password for the SMTP server if the user is using the SMTP delivery method.
const PopSMTPPassword = "POP_SMTP_PASSWORD" //nolint:gosec
// PopSMTPEncryption is the encryption type for the SMTP server if the user is using the SMTP delivery method.
const PopSMTPEncryption = "POP_SMTP_ENCRYPTION" //nolint:gosec
// PopSMTPInsecureSkipVerify is whether or not to skip TLS verification for the
// SMTP server if the user is using the SMTP delivery method.
const PopSMTPInsecureSkipVerify = "POP_SMTP_INSECURE_SKIP_VERIFY"
2023-06-15 10:30:20 -04:00
var (
2023-07-31 10:32:02 -04:00
from string
to []string
cc []string
bcc []string
subject string
body string
attachments []string
preview bool
unsafe bool
signature string
smtpHost string
smtpPort int
smtpUsername string
smtpPassword string
smtpEncryption string
smtpInsecureSkipVerify bool
resendAPIKey string
2023-06-15 10:30:20 -04:00
)
2023-06-15 09:45:14 -04:00
2023-06-13 23:31:19 -04:00
var rootCmd = &cobra.Command{
2023-07-12 12:07:31 -04:00
Use: "pop",
Short: "Send emails from your terminal",
Long: `Pop is a tool for sending emails from your terminal.`,
2023-06-13 23:31:19 -04:00
RunE: func(cmd *cobra.Command, args []string) error {
if !configExists() && smtpUsername == "" && smtpPassword == "" && resendAPIKey == "" {
if err := runOnboarding(); err != nil {
return err
}
}
2023-07-31 10:32:02 -04:00
var deliveryMethod DeliveryMethod
switch {
case resendAPIKey != "" && smtpUsername != "" && smtpPassword != "":
deliveryMethod = Unknown
2023-07-31 10:32:02 -04:00
case resendAPIKey != "":
deliveryMethod = Resend
case smtpUsername != "" && smtpPassword != "":
deliveryMethod = SMTP
if from == "" {
from = smtpUsername
}
2023-07-31 10:32:02 -04:00
}
switch deliveryMethod {
case None:
fmt.Printf("\n %s 未找到邮件配置\n\n", errorHeaderStyle.String())
fmt.Printf(" %s 请运行 %s 进行首次配置\n\n", commentStyle.Render("提示:"), inlineCodeStyle.Render("pop --config"))
cmd.SilenceUsage = true
cmd.SilenceErrors = true
return errors.New("missing mail configuration")
case Unknown:
fmt.Printf("\n %s Unknown delivery method.\n", errorHeaderStyle.String())
fmt.Printf("\n You have set both %s and %s delivery methods.", inlineCodeStyle.Render(ResendAPIKey), inlineCodeStyle.Render("POP_SMPT_*"))
fmt.Printf("\n Set only one of these environment variables.\n\n")
cmd.SilenceUsage = true
cmd.SilenceErrors = true
return errors.New("unknown delivery method")
2023-06-15 15:17:22 -04:00
}
if body == "" && hasStdin() {
2023-06-15 10:30:20 -04:00
b, err := io.ReadAll(os.Stdin)
if err != nil {
return err
}
body = string(b)
}
2023-07-11 10:21:28 -04:00
if signature != "" {
body += "\n\n" + signature
}
2023-07-10 12:37:02 -04:00
if len(to) > 0 && from != "" && subject != "" && body != "" && !preview {
2023-07-31 10:32:02 -04:00
var err error
switch deliveryMethod {
case SMTP:
err = sendSMTPEmail(to, cc, bcc, from, subject, body, attachments)
case Resend:
err = sendResendEmail(to, cc, bcc, from, subject, body, attachments)
default:
err = fmt.Errorf("unknown delivery method")
}
2023-06-15 10:30:20 -04:00
if err != nil {
2023-06-15 10:52:27 -04:00
cmd.SilenceUsage = true
cmd.SilenceErrors = true
fmt.Println(errorStyle.Render(err.Error()))
2023-06-15 10:30:20 -04:00
return err
}
2023-06-15 11:03:14 -04:00
fmt.Print(emailSummary(to, subject))
2023-06-15 10:30:20 -04:00
return nil
}
p := tea.NewProgram(NewModel(resend.SendEmailRequest{
From: from,
To: to,
Bcc: bcc,
Cc: cc,
Subject: subject,
Text: body,
Attachments: makeAttachments(attachments),
2023-07-31 10:32:02 -04:00
}, deliveryMethod))
2023-06-15 10:59:25 -04:00
m, err := p.Run()
2023-06-13 23:31:19 -04:00
if err != nil {
return err
}
2023-06-15 10:59:25 -04:00
mm := m.(Model)
if !mm.abort {
2023-07-31 10:32:02 -04:00
fmt.Print(emailSummary(strings.Split(mm.To.Value(), ToSeparator), mm.Subject.Value()))
}
2023-06-13 23:31:19 -04:00
return nil
},
}
2023-06-15 10:30:20 -04:00
// hasStdin returns whether there is data in stdin.
func hasStdin() bool {
stat, err := os.Stdin.Stat()
return err == nil && (stat.Mode()&os.ModeCharDevice) == 0
}
2023-07-12 12:07:31 -04:00
var (
// Version stores the build version of VHS at the time of package through
// -ldflags.
//
// go build -ldflags "-s -w -X=main.Version=$(VERSION)"
Version string
// CommitSHA stores the git commit SHA at the time of package through -ldflags.
CommitSHA string
)
2023-07-31 10:32:02 -04:00
// ManCmd is the cobra command for the manual.
2023-07-13 11:05:27 -04:00
var ManCmd = &cobra.Command{
Use: "man",
Short: "Generate man page",
Long: `To generate the man page`,
Args: cobra.NoArgs,
Hidden: true,
RunE: func(_ *cobra.Command, _ []string) error {
2023-07-13 11:05:27 -04:00
page, err := mcobra.NewManPage(1, rootCmd) // .
if err != nil {
return err
}
page = page.WithSection("Copyright", "© 2023 Charmbracelet, Inc.\n"+"Released under MIT License.")
fmt.Println(page.Build(roff.NewDocument()))
return nil
},
}
2023-06-15 10:30:20 -04:00
func init() {
rootCmd.AddCommand(ManCmd)
2023-07-12 12:07:31 -04:00
cfg, _ := loadConfig()
_ = cfg
2023-07-31 10:32:02 -04:00
rootCmd.Flags().StringSliceVar(&bcc, "bcc", []string{}, "BCC recipients")
rootCmd.Flags().StringSliceVar(&cc, "cc", []string{}, "CC recipients")
2023-06-15 10:30:20 -04:00
rootCmd.Flags().StringSliceVarP(&attachments, "attach", "a", []string{}, "Email's attachments")
2023-06-15 11:27:20 -04:00
rootCmd.Flags().StringSliceVarP(&to, "to", "t", []string{}, "Recipients")
rootCmd.Flags().StringVarP(&body, "body", "b", "", "Email's contents")
2023-07-31 10:32:02 -04:00
envFrom := os.Getenv(PopFrom)
if envFrom == "" {
envFrom = getDefaultFromEmail()
}
from = envFrom
2023-07-31 10:32:02 -04:00
rootCmd.Flags().StringVarP(&from, "from", "f", envFrom, "Email's sender"+commentStyle.Render("($"+PopFrom+")"))
2023-06-15 10:30:20 -04:00
rootCmd.Flags().StringVarP(&subject, "subject", "s", "", "Email's subject")
2023-07-31 10:32:02 -04:00
rootCmd.Flags().BoolVar(&preview, "preview", false, "Whether to preview the email before sending")
envUnsafe := os.Getenv(PopUnsafeHTML) == "true"
if !envUnsafe {
envUnsafe = cfg.UnsafeHTML
}
rootCmd.Flags().BoolVarP(&unsafe, "unsafe", "u", envUnsafe, "Whether to allow unsafe HTML in the email body, also enable some extra markdown features (Experimental)")
2023-07-31 10:32:02 -04:00
envSignature := os.Getenv(PopSignature)
if envSignature == "" {
envSignature = cfg.Signature
}
2023-07-31 10:32:02 -04:00
rootCmd.Flags().StringVarP(&signature, "signature", "x", envSignature, "Signature to display at the end of the email."+commentStyle.Render("($"+PopSignature+")"))
envSMTPHost := os.Getenv(PopSMTPHost)
envSMTPPort, _ := strconv.Atoi(os.Getenv(PopSMTPPort))
envSMTPUsername := os.Getenv(PopSMTPUsername)
envSMTPPassword := os.Getenv(PopSMTPPassword)
envSMTPEncryption := os.Getenv(PopSMTPEncryption)
envInsecureSkipVerify := os.Getenv(PopSMTPInsecureSkipVerify) == "true"
defaultAccounts, _ := getAccounts()
defaultAccount := getDefaultAccount(defaultAccounts, cfg.From.Account)
if envSMTPHost == "" && defaultAccount != nil {
envSMTPHost = defaultAccount.SMTP.Host
}
if envSMTPPort == 0 && defaultAccount != nil {
envSMTPPort = defaultAccount.SMTP.Port
if envSMTPPort == 0 {
envSMTPPort = 587
}
2023-07-31 10:32:02 -04:00
}
if envSMTPUsername == "" && defaultAccount != nil {
envSMTPUsername = defaultAccount.SMTP.Username
}
if envSMTPPassword == "" && defaultAccount != nil {
envSMTPPassword = defaultAccount.SMTP.Password
}
if envSMTPEncryption == "" && defaultAccount != nil {
envSMTPEncryption = defaultAccount.SMTP.Encryption
if envSMTPEncryption == "" {
envSMTPEncryption = "starttls"
}
}
if !envInsecureSkipVerify && defaultAccount != nil {
envInsecureSkipVerify = defaultAccount.SMTP.InsecureSkipVerify
}
smtpHost = envSMTPHost
smtpPort = envSMTPPort
smtpUsername = envSMTPUsername
smtpPassword = envSMTPPassword
smtpEncryption = envSMTPEncryption
smtpInsecureSkipVerify = envInsecureSkipVerify
rootCmd.Flags().StringVarP(&smtpHost, "smtp.host", "H", envSMTPHost, "Host of the SMTP server"+commentStyle.Render("($"+PopSMTPHost+")"))
rootCmd.Flags().IntVarP(&smtpPort, "smtp.port", "P", envSMTPPort, "Port of the SMTP server"+commentStyle.Render("($"+PopSMTPPort+")"))
rootCmd.Flags().StringVarP(&smtpUsername, "smtp.username", "U", envSMTPUsername, "Username of the SMTP server"+commentStyle.Render("($"+PopSMTPUsername+")"))
rootCmd.Flags().StringVarP(&smtpPassword, "smtp.password", "p", envSMTPPassword, "Password of the SMTP server"+commentStyle.Render("($"+PopSMTPPassword+")"))
rootCmd.Flags().StringVarP(&smtpEncryption, "smtp.encryption", "e", envSMTPEncryption, "Encryption type of the SMTP server (starttls, ssl, or none)"+commentStyle.Render("($"+PopSMTPEncryption+")"))
2023-07-31 10:32:02 -04:00
rootCmd.Flags().BoolVarP(&smtpInsecureSkipVerify, "smtp.insecure", "i", envInsecureSkipVerify, "Skip TLS verification with SMTP server"+commentStyle.Render("($"+PopSMTPInsecureSkipVerify+")"))
envResendAPIKey := os.Getenv(ResendAPIKey)
rootCmd.Flags().StringVarP(&resendAPIKey, "resend.key", "r", envResendAPIKey, "API key for the Resend.com"+commentStyle.Render("($"+ResendAPIKey+")"))
2023-07-12 12:07:31 -04:00
rootCmd.CompletionOptions.HiddenDefaultCmd = true
var configCmd = &cobra.Command{
Use: "config",
Short: "配置或重新配置 Pop",
Long: `打开交互式配置向导来设置或更新 Pop 的配置。`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runOnboarding()
},
}
rootCmd.AddCommand(configCmd)
var inboxCmd = &cobra.Command{
Use: "inbox",
Short: "打开收件箱",
Long: `查看配置邮箱的收件箱,显示未读邮件列表。`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runInbox(7)
},
}
inboxCmd.Flags().IntP("days", "d", 7, "显示最近几天的未读邮件")
rootCmd.AddCommand(inboxCmd)
var historyCmd = &cobra.Command{
Use: "history",
Short: "查看发送历史",
Long: `查看已发送或草稿邮件的历史记录。`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runHistory()
},
}
rootCmd.AddCommand(historyCmd)
2023-07-12 12:07:31 -04:00
if len(CommitSHA) >= 7 { //nolint:gomnd
vt := rootCmd.VersionTemplate()
rootCmd.SetVersionTemplate(vt[:len(vt)-1] + " (" + CommitSHA[0:7] + ")\n")
}
if Version == "" {
if info, ok := debug.ReadBuildInfo(); ok && info.Main.Sum != "" {
Version = info.Main.Version
} else {
Version = "unknown (built from source)"
}
}
rootCmd.Version = Version
2023-06-15 10:30:20 -04:00
}
2023-06-13 23:31:19 -04:00
func main() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}