M go.mod => go.mod +1 -0
@@ 5,6 5,7 @@ go 1.16
require (
github.com/360EntSecGroup-Skylar/excelize v1.4.1
github.com/alecthomas/kong v0.2.16
+ github.com/emersion/go-message v0.14.1
github.com/gookit/color v1.4.2
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
M go.sum => go.sum +9 -0
@@ 5,6 5,10 @@ github.com/alecthomas/kong v0.2.16/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QL
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/emersion/go-message v0.14.1 h1:j3rj9F+7VtXE9c8P5UHBq8FTHLW/AjnmvSRre6AHoYI=
+github.com/emersion/go-message v0.14.1/go.mod h1:N1JWdZQ2WRUalmdHAX308CWBq747VJ8oUorFI3VCBwU=
+github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
+github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/gookit/color v1.4.2 h1:tXy44JFSFkKnELV6WaMo/lLfu/meqITX3iAV52do7lk=
github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ 13,6 17,8 @@ github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
+github.com/martinlindhe/base36 v1.1.0 h1:cIwvvwYse/0+1CkUPYH5ZvVIYG3JrILmQEIbLuar02Y=
+github.com/martinlindhe/base36 v1.1.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ 30,6 36,7 @@ github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:s
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.3-0.20181224173747-660f15d67dbb/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ 37,6 44,8 @@ github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHg
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 h1:Bli41pIlzTzf3KEY06n+xnzK/BESIg2ze4Pgfh/aI8c=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
M main.go => main.go +37 -5
@@ 26,6 26,18 @@ var CLI struct {
Month string `arg optional help:"select the month to export in format: YYYYMM (default is the previous month)"`
Output string `optional short:"o" default:"report.xlsx" help:"output file path"`
} `cmd help:"export data to a spreadsheet"`
+ SendReport struct {
+ Month string `arg optional help:"select the month to export in format: YYYYMM (default is the previous month)"`
+ Output string `optional short:"o" default:"report.xlsx" help:"attachment file name"`
+ SmtpServer string `help:"specifies the outgoing SMTP server to use" env:"SMTP_SERVER"`
+ SmtpPort int `default:"25" help:"specifies a port different from the default port (SMTP servers typically listen to smtp port 25, but may also listen to submission port 587, or the common SSL smtp port 465)" env:"SMTP_PORT"`
+ SmtpAuth string `enum:"PLAIN,CRAM-MD5" help:"SMTP authentication mechanism" env:"SMTP_AUTH"`
+ SmtpUser string `optional help:"username for SMTP-AUTH. if a username is not specified, then authentication is not attempted." env:"SMTP_USER"`
+ SmtpPass string `optional help:"password for SMTP-AUTH. If no argument is specified, then the empty string is used as the password" env:"SMTP_PASS"`
+ SmtpPassCmd string `optional help:"command that prints the password for SMTP-AUTH" env:"SMTP_PASS_CMD"`
+ To []string `required help:"specify the primary recipients of the emails generated"`
+ From string `required help:"specify the sender of the emails" env:"SENDMAIL_FROM"`
+ } `cmd help:"sends the report by email"`
DataFile string `optional short:"d" help:"path to the wt data file (default:$HOME/.local/wt.data)" predictor:"file"`
InstallCompletions struct {
Uninstall bool
@@ 47,6 59,21 @@ func main() {
"o": predict.Files("*.xslt"),
},
},
+ "send-report": {
+ Args: &monthsPredictor{},
+ Flags: map[string]complete.Predictor{
+ "output": predict.Files("*.xslt"),
+ "o": predict.Files("*.xslt"),
+ "smtp-server": predict.Something,
+ "smtp-port": predict.Something,
+ "smtp-auth": predict.Set([]string{"PLAIN", "CRAM-MD5"}),
+ "smtp-user": predict.Something,
+ "smtp-pass": predict.Something,
+ "smtp-pass-cmd": predict.Something,
+ "to": predict.Something,
+ "from": predict.Something,
+ },
+ },
},
Flags: map[string]complete.Predictor{
"data-file": predict.Files("*"),
@@ 66,27 93,32 @@ func main() {
switch ctx.Command() {
case "start <project> <description>":
if err := start(); err != nil {
- fmt.Fprintf(os.Stderr, "%s", err)
+ fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(1)
}
case "end":
if err := end(); err != nil {
- fmt.Fprintf(os.Stderr, "%s", err)
+ fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(1)
}
case "report", "report <month>":
if err := report(); err != nil {
- fmt.Fprintf(os.Stderr, "%s", err)
+ fmt.Fprintf(os.Stderr, "%s\n", err)
+ os.Exit(1)
+ }
+ case "send-report", "send-report <month>":
+ if err := sendreport(); err != nil {
+ fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(1)
}
case "status":
if err := status(); err != nil {
- fmt.Fprintf(os.Stderr, "%s", err)
+ fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(1)
}
case "install-completions":
if err := install.Install("wt"); err != nil {
- fmt.Fprintf(os.Stderr, "%s", err)
+ fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(1)
}
default:
M report.go => report.go +18 -11
@@ 11,14 11,27 @@ import (
)
func report() error {
+ xlsx, _, err := createReport(CLI.Report.Month)
+ if err != nil {
+ return err
+ }
+ // Save xlsx file by the given path.
+ if err := xlsx.SaveAs(CLI.Report.Output); err != nil {
+ return fmt.Errorf("writing file %s: %s", CLI.Report.Output, err)
+ }
+ open.Run(CLI.Report.Output)
+ return nil
+}
+
+func createReport(month string) (*excelize.File, string, error) {
now := time.Now()
y, m, _ := now.Date()
es, err := loadEntries()
if err != nil {
- return fmt.Errorf("loading entries: %w", err)
+ return nil, "", fmt.Errorf("loading entries: %w", err)
}
var ry, rm int
- if CLI.Report.Month == "" {
+ if month == "" {
if m == time.January {
ry = y - 1
rm = 12
@@ 27,9 40,9 @@ func report() error {
rm = int(m) - 1
}
} else {
- t, err := time.Parse("200601", CLI.Report.Month)
+ t, err := time.Parse("200601", month)
if err != nil {
- return fmt.Errorf("parsing report month: %w", err)
+ return nil, "", fmt.Errorf("parsing report month: %w", err)
}
ty, tm, _ := t.Date()
ry = ty
@@ 177,14 190,8 @@ func report() error {
sheetName, pivotStart, pivotEnd-1,
),
)
-
xlsx.SetActiveSheet(index)
- // Save xlsx file by the given path.
- if err := xlsx.SaveAs(CLI.Report.Output); err != nil {
- return fmt.Errorf("writing file %s: %s", CLI.Report.Output, err)
- }
- open.Run(CLI.Report.Output)
- return nil
+ return xlsx, fmt.Sprintf("%02d/%04d", rm, ry), nil
}
func groupEntries(es Entries) (ees Entries) {
A send_report.go => send_report.go +121 -0
@@ 0,0 1,121 @@
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "net/smtp"
+ "os"
+ "os/exec"
+ "time"
+
+ "github.com/emersion/go-message/mail"
+)
+
+func sendreport() error {
+ xlsx, month, err := createReport(CLI.SendReport.Month)
+ if err != nil {
+ return err
+ }
+ f, err := os.CreateTemp("", "*.xlsx")
+ if err != nil {
+ return fmt.Errorf("cannot create temp file for report: %w", err)
+ }
+ f.Close()
+ defer os.Remove(f.Name())
+ if err := xlsx.SaveAs(f.Name()); err != nil {
+ return fmt.Errorf("writing temp file %s: %s", f.Name(), err)
+ }
+
+ var msg bytes.Buffer
+ from := []*mail.Address{{Address: CLI.SendReport.From}}
+ to := []*mail.Address{}
+ for _, toAddr := range CLI.SendReport.To {
+ to = append(to, &mail.Address{Address: toAddr})
+ }
+
+ // Create our mail header
+ var h mail.Header
+ h.SetDate(time.Now())
+ h.SetAddressList("From", from)
+ h.SetAddressList("To", to)
+ h.SetSubject("report " + month)
+
+ mw, err := mail.CreateWriter(&msg, h)
+ if err != nil {
+ return err
+ }
+ // Create a text part
+ tw, err := mw.CreateInline()
+ if err != nil {
+ return err
+ }
+ var th mail.InlineHeader
+ th.Set("Content-Type", "text/plain")
+ w, err := tw.CreatePart(th)
+ if err != nil {
+ return err
+ }
+ io.WriteString(w, "report "+month)
+ w.Close()
+ tw.Close()
+ // Create an attachment
+ var ah mail.AttachmentHeader
+ ah.Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
+ ah.SetFilename(CLI.SendReport.Output)
+ w, err = mw.CreateAttachment(ah)
+ if err != nil {
+ return err
+ }
+ f, err = os.Open(f.Name())
+ if err != nil {
+ return err
+ }
+ if _, err := io.Copy(w, f); err != nil {
+ return err
+ }
+ f.Close()
+ w.Close()
+ mw.Close()
+
+ var auth smtp.Auth
+ if CLI.SendReport.SmtpUser != "" {
+ pass, err := getSmtpPass()
+ if err != nil {
+ return fmt.Errorf("smtp pass: %w", err)
+ }
+ switch CLI.SendReport.SmtpAuth {
+ case "PLAIN":
+ auth = smtp.PlainAuth(
+ "",
+ CLI.SendReport.SmtpUser,
+ pass,
+ CLI.SendReport.SmtpServer,
+ )
+ case "CRAM-MD5":
+ auth = smtp.CRAMMD5Auth(CLI.SendReport.SmtpUser, pass)
+ }
+ }
+
+ if err := smtp.SendMail(
+ fmt.Sprintf("%s:%d", CLI.SendReport.SmtpServer, CLI.SendReport.SmtpPort),
+ auth,
+ CLI.SendReport.From,
+ CLI.SendReport.To,
+ msg.Bytes(),
+ ); err != nil {
+ return fmt.Errorf("sending email: %w", err)
+ }
+ return nil
+}
+
+func getSmtpPass() (string, error) {
+ if CLI.SendReport.SmtpPassCmd == "" {
+ return CLI.SendReport.SmtpPass, nil
+ }
+ output, err := exec.Command("sh", "-c", CLI.SendReport.SmtpPassCmd).CombinedOutput()
+ if err != nil {
+ return "", err
+ }
+ return string(output), nil
+}