~ghost08/wt

be6e1d3361d2f4641d2df3701802ed2b74433390 — Vladimír Magyar 2 years ago b3cf601
Add send-report functionality
5 files changed, 186 insertions(+), 16 deletions(-)

M go.mod
M go.sum
M main.go
M report.go
A send_report.go
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
}