package main import ( "context" "flag" "io" "mime" "net/http" "net/url" "os" "path/filepath" "strings" "time" "git.icedream.tech/icedream/oneandone-billing-mailer/pkg/api" "git.icedream.tech/icedream/oneandone-billing-mailer/pkg/appcontext" "git.icedream.tech/icedream/oneandone-billing-mailer/pkg/auth" "git.icedream.tech/icedream/oneandone-billing-mailer/pkg/auth/authcache" "git.icedream.tech/icedream/oneandone-billing-mailer/pkg/auth/authcache/fileauthcache" "git.icedream.tech/icedream/oneandone-billing-mailer/pkg/backend" "git.icedream.tech/icedream/oneandone-billing-mailer/pkg/environment" "github.com/go-logr/logr" "github.com/go-logr/zapr" "go.uber.org/zap" "golang.org/x/oauth2" ) const ( // webviewDebug toggles the debug mode in the Webview library. webviewDebug = false // apiDebug toggles the debug mode in the OpenAPI client. apiDebug = false ) // dateFormat is the format used to pass dates to the 1&1 API const dateFormat = "2006-01-02" // main is the main entrypoint. func main() { ctx := context.Background() // load logger zapLogger, err := zap.NewDevelopment() if err != nil { os.Stderr.WriteString("Could not initialize zap logger\n") os.Exit(1) return } generalLog := zapr.NewLogger(zapLogger) ctx = logr.NewContext(ctx, generalLog) log := generalLog.WithName("app") // parse cmdline flag.Parse() // initialize oauth stuff for API oauth, err := auth.NewOAuthFromEnvironment(ctx) if err != nil { log.Error(err, "Failed to initialize central login helper") return } // Try and get an initial token first tokenCache := fileauthcache.NewFileAuthCache("token.json") token, err := tokenCache.Fetch() if err != nil { log.Error(err, "Failed to fetch token from cache") return } if token == nil { // We need to fetch an initial token tokenSource, err := oauth.TokenSource(nil, requestUserConsentFunc(ctx)) if err != nil { log.Error(err, "Failed to set up OAuth token source") return } cacheTokenSource := authcache.NewCacheTokenSource(ctx, tokenCache, tokenSource) token, err = cacheTokenSource.Token() if err != nil { log.Error(err, "Failed to fetch initial token") return } } // Set up new token source only for refreshing tokenSource, err := oauth.TokenSource(token, nil) if err != nil { log.Error(err, "Failed to set up OAuth token source") return } cacheTokenSource := authcache.NewCacheTokenSource(ctx, tokenCache, tokenSource) reuseTokenSource := oauth2.ReuseTokenSource(token, cacheTokenSource) client := oauth2.NewClient(ctx, reuseTokenSource) client.Transport = backend.BackendRequestTransport(client.Transport) // MSSA invoice requests from here appctx, err := appcontext.GetEnvironmentContext() if err != nil { log.Error(err, "Failed to fetch app env context") return } mssaBaseURL, err := appctx.MSSA.BaseURL() if err != nil { log.Error(err, "Failed to parse MSSA base URL") return } auth := context.WithValue(ctx, api.ContextOAuth2, reuseTokenSource) apiConfig := api.NewConfiguration() apiConfig.HTTPClient = client apiConfig.Debug = apiDebug apiConfig.UserAgent = environment.UserAgent() apiClient := api.NewAPIClient(apiConfig) now := time.Now() thisMonthStartDate := now. // go to first day of this month AddDate(0, 0, 1-now.Day()) nextMonthStartDate := thisMonthStartDate. // add 1 month AddDate(0, 1, 0) apiReq := apiClient.InvoicesApi.GetInvoicesInInterval(auth, thisMonthStartDate.Format(dateFormat), nextMonthStartDate.Format(dateFormat)). PerPage(1) apiResp, resp, err := apiReq.Page(1).Execute() if err != nil { panic(err) } if resp.StatusCode != http.StatusOK { panic(resp.Status) } for _, invoice := range apiResp.GetInvoice() { // skip entries without invoice document doc := invoice.Links.InvoiceDocument if doc == nil { continue } ext := ".pdf" if doc.Type != nil { exts, err := mime.ExtensionsByType(*doc.Type) if err != nil { log.V(1).Error(err, "Failed to fetch extensions for type", "type", *doc.Type) } else if len(exts) > 0 { ext = exts[0] } } // make sure extension starts with dot if !strings.HasPrefix(ext, ".") { ext = "." + ext } title := "Rechnung" if doc.Title != nil { title = *doc.Title } fileName := filepath.Clean(title) + ext u, err := url.Parse(doc.Href) if err != nil { log.V(1).Error(err, "Failed to parse invoice document hyperlink reference, skipping", "url", doc.Href) continue } u = mssaBaseURL.ResolveReference(u) // open PDF on server resp, err := apiClient.GetConfig().HTTPClient.Get(u.String()) if err != nil { log.Error(err, "Invoice PDF download request failed, skipping", "fileName", fileName) continue } if resp.StatusCode != http.StatusOK { resp.Body.Close() log.Error(err, "Unexpected status code for invoice PDF download request, skipping", "fileName", fileName, "statusCode", resp.StatusCode) continue } // write PDF to file as we download f, err := os.Create(fileName) if err != nil { resp.Body.Close() log.Error(err, "Could not create invoice PDF", "fileName", fileName) continue } _, err = io.Copy(f, resp.Body) resp.Body.Close() if err != nil { os.Remove(f.Name()) log.Error(err, "Could not write to invoice PDF", "fileName", fileName) } err = f.Close() if err != nil { os.Remove(f.Name()) log.Error(err, "Could not complete writing to invoice PDF", "fileName", fileName) } } }