From 939c14bfd6687fcfe838b1e1e2d7acbd458c6943 Mon Sep 17 00:00:00 2001 From: PatWie Date: Mon, 13 May 2019 16:06:01 +0200 Subject: [PATCH 1/2] simplify configuration, semantic version, multi-worker AMQP had several options in the config which should not be altered. The version gets its own endpoint and uses semver. Starting the background workers now supports the flag "-n X" to start X workers within process to avoid having multiple detached screens. --- .drone.yml | 4 +-- .infomark.example.yml | 3 --- .infomark.yml.ci | 3 --- api/app/common.go | 16 ++++++++++++ api/app/common_responses.go | 25 ++++++++++++++++-- api/app/router.go | 23 ++++++++++------- api/app/submission_producer.go | 6 ++--- api/worker.go | 38 +++++++++++++++++---------- cmd/console/submission_cmd.go | 12 ++++----- cmd/root.go | 8 +++--- cmd/serve.go | 2 +- cmd/work.go | 12 ++++++--- service/consumer.go | 7 ++++- symbol/version.go | 47 ++++++++++++++++++++++++++++++++++ 14 files changed, 157 insertions(+), 49 deletions(-) create mode 100644 symbol/version.go diff --git a/.drone.yml b/.drone.yml index 2cb397c..3641467 100644 --- a/.drone.yml +++ b/.drone.yml @@ -23,7 +23,7 @@ steps: pull: default image: golang commands: - - sed -i 's/"X-INFOMARK-VERSION", "0.0.1"/"X-INFOMARK-VERSION", "${DRONE_COMMIT_SHA}"/g' api/app/router.go + - sed -i 's/"YXZ"/"${DRONE_COMMIT_SHA}"/g' symbol/version.go - go version - go build infomark.go environment: @@ -126,6 +126,6 @@ services: --- kind: signature -hmac: bec11f987bcc8b10e923674bbb63e983c274ee5b007d2f2ae9a703f8f6e829cd +hmac: 79461cde6a81a50bc7e6ad292872f4a10b9126d206196430cd4accaa4f6a61b9 ... diff --git a/.infomark.example.yml b/.infomark.example.yml index 360027c..348c956 100644 --- a/.infomark.example.yml +++ b/.infomark.example.yml @@ -2,9 +2,6 @@ # ------------------------------------------------------------------------------ rabbitmq_connection: amqp://user:password@localhost:5672/ -rabbitmq_exchange: test-exchange -rabbitmq_exchangeType: direct -rabbitmq_queue: test-queue rabbitmq_key: test-key # backend diff --git a/.infomark.yml.ci b/.infomark.yml.ci index 851b09a..141cac2 100644 --- a/.infomark.yml.ci +++ b/.infomark.yml.ci @@ -2,9 +2,6 @@ # ------------------------------------------------------------------------------ rabbitmq_connection: amqp://user:password@localhost:5672/ -rabbitmq_exchange: test-exchange -rabbitmq_exchangeType: direct -rabbitmq_queue: test-queue rabbitmq_key: test-key diff --git a/api/app/common.go b/api/app/common.go index 0e43af5..c1b67be 100644 --- a/api/app/common.go +++ b/api/app/common.go @@ -52,6 +52,22 @@ func (rs *CommonResource) PingHandler(w http.ResponseWriter, r *http.Request) { w.Write([]byte("pong")) } +// VersionHandler is public endpoint for +// URL: /version +// METHOD: get +// TAG: common +// RESPONSE: 200,versionResponse +// SUMMARY: all version information +func (rs *CommonResource) VersionHandler(w http.ResponseWriter, r *http.Request) { + // render JSON reponse + if err := render.Render(w, r, newVersionResponse()); err != nil { + render.Render(w, r, ErrRender(err)) + return + } + + render.Status(r, http.StatusOK) +} + // PrivacyStatementHandler is public endpoint for // URL: /privacy_statement // METHOD: get diff --git a/api/app/common_responses.go b/api/app/common_responses.go index 47159d1..aa039e0 100644 --- a/api/app/common_responses.go +++ b/api/app/common_responses.go @@ -20,14 +20,16 @@ package app import ( "net/http" + + "github.com/cgtuebingen/infomark-backend/symbol" ) -// courseResponse is the response payload for course management. +// rawResponse is the response payload for course management. type rawResponse struct { Text string `json:"text" example:"some text"` } -// Render post-processes a courseResponse. +// Render post-processes a rawResponse. func (body *rawResponse) Render(w http.ResponseWriter, r *http.Request) error { return nil } @@ -38,3 +40,22 @@ func newRawResponse(text string) *rawResponse { Text: text, } } + +// versionResponse is the response payload for course management. +type versionResponse struct { + Commit string `json:"commit" example:"d725269a8a7498aae1dbb07786bed4c88b002661"` + Version string `json:"version" example:"1"` +} + +// newVersionResponse creates a response from a course model. +func newVersionResponse() *versionResponse { + return &versionResponse{ + Commit: symbol.GitCommit, + Version: symbol.Version.String(), + } +} + +// Render post-processes a versionResponse. +func (body *versionResponse) Render(w http.ResponseWriter, r *http.Request) error { + return nil +} diff --git a/api/app/router.go b/api/app/router.go index 9f57422..cc3d0aa 100644 --- a/api/app/router.go +++ b/api/app/router.go @@ -30,6 +30,7 @@ import ( "github.com/cgtuebingen/infomark-backend/auth/authenticate" "github.com/cgtuebingen/infomark-backend/auth/authorize" + "github.com/cgtuebingen/infomark-backend/symbol" "github.com/go-chi/chi" "github.com/go-chi/chi/middleware" "github.com/go-chi/cors" @@ -74,21 +75,24 @@ func LoggingMiddleware(next http.Handler) http.Handler { start := time.Now() next.ServeHTTP(w, r) end := time.Now() - log.WithFields(logrus.Fields{ - "method": r.Method, - // "proto": r.Proto, - "agent": r.UserAgent(), - "remote": r.RemoteAddr, - "latency": end.Sub(start), - "time": end.Format(time.RFC3339), - }).Info(r.RequestURI) + if r.RequestURI != "/metrics" { + log.WithFields(logrus.Fields{ + "method": r.Method, + // "proto": r.Proto, + "agent": r.UserAgent(), + "remote": r.RemoteAddr, + "latency": end.Sub(start), + "time": end.Format(time.RFC3339), + }).Info(r.RequestURI) + } }) } // VersionMiddleware writes the current API version to the headers. func VersionMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("X-INFOMARK-VERSION", "0.0.1") + w.Header().Set("X-INFOMARK-VERSION", symbol.Version.String()) + w.Header().Set("X-INFOMARK-Commit", symbol.GitCommit) next.ServeHTTP(w, r) }) } @@ -171,6 +175,7 @@ func New(db *sqlx.DB, log bool) (*chi.Mux, error) { r.Post("/auth/confirm_email", appAPI.Auth.ConfirmEmailHandler) r.Post("/account", appAPI.Account.CreateHandler) r.Get("/ping", appAPI.Common.PingHandler) + r.Get("/version", appAPI.Common.VersionHandler) r.Get("/privacy_statement", appAPI.Common.PrivacyStatementHandler) }) diff --git a/api/app/submission_producer.go b/api/app/submission_producer.go index b759a34..9a5d379 100644 --- a/api/app/submission_producer.go +++ b/api/app/submission_producer.go @@ -44,9 +44,9 @@ func init() { cfg := &service.Config{ Connection: viper.GetString("rabbitmq_connection"), - Exchange: viper.GetString("rabbitmq_exchange"), - ExchangeType: viper.GetString("rabbitmq_exchangeType"), - Queue: viper.GetString("rabbitmq_queue"), + Exchange: "infomark-worker-exchange", + ExchangeType: "direct", + Queue: "infomark-worker-submissions", Key: viper.GetString("rabbitmq_key"), Tag: "SimpleSubmission", } diff --git a/api/worker.go b/api/worker.go index 1d7a6cd..226de26 100644 --- a/api/worker.go +++ b/api/worker.go @@ -24,16 +24,19 @@ import ( background "github.com/cgtuebingen/infomark-backend/api/worker" "github.com/cgtuebingen/infomark-backend/service" + "github.com/sirupsen/logrus" "github.com/spf13/viper" ) // Worker provides a background worker -type Worker struct{} +type Worker struct { + NumInstances int +} // NewWorker creates and configures an background worker -func NewWorker() (*Worker, error) { +func NewWorker(numInstances int) (*Worker, error) { log.Println("configuring worker...") - return &Worker{}, nil + return &Worker{NumInstances: numInstances}, nil } // Start runs ListenAndServe on the http.Worker with graceful shutdown. @@ -42,25 +45,34 @@ func (srv *Worker) Start() { cfg := &service.Config{ Connection: viper.GetString("rabbitmq_connection"), - Exchange: viper.GetString("rabbitmq_exchange"), - ExchangeType: viper.GetString("rabbitmq_exchangeType"), - Queue: viper.GetString("rabbitmq_queue"), + Exchange: "infomark-worker-exchange", + ExchangeType: "direct", + Queue: "infomark-worker-submissions", Key: viper.GetString("rabbitmq_key"), Tag: "SimpleSubmission", } - consumer, _ := service.NewConsumer(cfg, background.DefaultSubmissionHandler.Handle) - deliveries, err := consumer.Setup() - if err != nil { - panic(err) - } + consumers := []*service.Consumer{} - go consumer.HandleLoop(deliveries) + for i := 0; i < srv.NumInstances; i++ { + log.WithFields(logrus.Fields{"instance": i}).Info("start") + consumer, _ := service.NewConsumer(cfg, background.DefaultSubmissionHandler.Handle, i) + deliveries, err := consumer.Setup() + if err != nil { + panic(err) + } + consumers = append(consumers, consumer) + go consumers[i].HandleLoop(deliveries) + } quit := make(chan os.Signal, 1) signal.Notify(quit, os.Interrupt) sig := <-quit log.Println("Shutting down Worker... Reason:", sig) - consumer.Shutdown() + + for i := 0; i < srv.NumInstances; i++ { + consumers[i].Shutdown() + } + log.Println("Worker gracefully stopped") } diff --git a/cmd/console/submission_cmd.go b/cmd/console/submission_cmd.go index e63b830..e38ac6f 100644 --- a/cmd/console/submission_cmd.go +++ b/cmd/console/submission_cmd.go @@ -75,9 +75,9 @@ var SubmissionTriggerCmd = &cobra.Command{ cfg := &service.Config{ Connection: viper.GetString("rabbitmq_connection"), - Exchange: viper.GetString("rabbitmq_exchange"), - ExchangeType: viper.GetString("rabbitmq_exchangeType"), - Queue: viper.GetString("rabbitmq_queue"), + Exchange: "infomark-worker-exchange", + ExchangeType: "direct", + Queue: "infomark-worker-submissions", Key: viper.GetString("rabbitmq_key"), Tag: "SimpleSubmission", } @@ -242,9 +242,9 @@ This triggers all [kind]-tests again (private, public). cfg := &service.Config{ Connection: viper.GetString("rabbitmq_connection"), - Exchange: viper.GetString("rabbitmq_exchange"), - ExchangeType: viper.GetString("rabbitmq_exchangeType"), - Queue: viper.GetString("rabbitmq_queue"), + Exchange: "infomark-worker-exchange", + ExchangeType: "direct", + Queue: "infomark-worker-submissions", Key: viper.GetString("rabbitmq_key"), Tag: "SimpleSubmission", } diff --git a/cmd/root.go b/cmd/root.go index 8707631..c363af9 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -31,10 +31,12 @@ import ( var RootCmd = &cobra.Command{ Use: "infomark", Short: "A CI based course framework", - Long: `InfoMark distributes exercise sheets in an course management system and -tests students homework submission for these exercises sheet automatically. - + Long: `InfoMark is a a scalable, modern and open-source +online course management system supporting auto-testing/grading of +programming assignments and distributing exercise sheets. The infomark-server is the REST api backend for the course distributing system. + +Complete documentation is available at https://infomark.org/. `, } diff --git a/cmd/serve.go b/cmd/serve.go index a43a79d..85bb665 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -28,7 +28,7 @@ import ( // serveCmd represents the serve command var serveCmd = &cobra.Command{ Use: "serve", - Short: "start http server with configured api", + Short: "start the infomark RESTful JSON backend server with configured api", Long: `Starts a http server and serves the configured api`, Run: func(cmd *cobra.Command, args []string) { server, err := api.NewServer() diff --git a/cmd/work.go b/cmd/work.go index 6ac7680..0bf7525 100644 --- a/cmd/work.go +++ b/cmd/work.go @@ -22,24 +22,30 @@ import ( "log" "github.com/cgtuebingen/infomark-backend/api" + "github.com/spf13/cobra" ) +var numWorkers = 1 + var workCmd = &cobra.Command{ Use: "work", Short: "start a worker", - Long: `Starts a background worker which will use docker to test submissions`, + Long: `Starts a background worker which will use docker to test submissions. +Can be used with the flag "-n" to start multiple workers within one process. +`, Run: func(cmd *cobra.Command, args []string) { - worker, err := api.NewWorker() + worker, err := api.NewWorker(numWorkers) if err != nil { log.Fatal(err) } worker.Start() - }, } func init() { + + workCmd.Flags().IntVarP(&numWorkers, "number", "n", 1, "number of workers within one routine") RootCmd.AddCommand(workCmd) } diff --git a/service/consumer.go b/service/consumer.go index 8083aa9..992f37c 100644 --- a/service/consumer.go +++ b/service/consumer.go @@ -33,11 +33,13 @@ type Consumer struct { channel *amqp.Channel done chan error + instanceID int + handleFunc func(body []byte) error } // NewConsumer creates new consumer which can act on AMPQ messages -func NewConsumer(cfg *Config, handleFunc func(body []byte) error) (*Consumer, error) { +func NewConsumer(cfg *Config, handleFunc func(body []byte) error, instanceID int) (*Consumer, error) { consumer := &Consumer{ conn: nil, @@ -45,6 +47,8 @@ func NewConsumer(cfg *Config, handleFunc func(body []byte) error) (*Consumer, er done: make(chan error), handleFunc: handleFunc, + instanceID: instanceID, + Config: cfg, } @@ -55,6 +59,7 @@ func NewConsumer(cfg *Config, handleFunc func(body []byte) error) (*Consumer, er func (c *Consumer) Setup() (<-chan amqp.Delivery, error) { logger := log.WithFields(logrus.Fields{ // "connection": c.Config.Connection, + "instance": c.instanceID, "exchange": c.Config.Exchange, "exchangetype": c.Config.ExchangeType, "queue": c.Config.Queue, diff --git a/symbol/version.go b/symbol/version.go new file mode 100644 index 0000000..e848417 --- /dev/null +++ b/symbol/version.go @@ -0,0 +1,47 @@ +// InfoMark - a platform for managing courses with +// distributing exercise sheets and testing exercise submissions +// Copyright (C) 2019 ComputerGraphics Tuebingen +// Authors: Patrick Wieschollek +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package symbol + +import ( + "github.com/coreos/go-semver/semver" +) + +var ( + // GitCommit is the git commit that was compiled + GitCommit string = "YXZ" + // VersionMajor is for an API incompatible changes. + VersionMajor int64 = 0 + // VersionMinor is for functionality in a backwards-compatible manner. + VersionMinor int64 = 0 + // VersionPatch is for backwards-compatible bug fixes. + VersionPatch int64 = 1 + // VersionPre indicates prerelease. + VersionPre = "beta" + // VersionDev indicates development branch. Releases will be empty string. + VersionDev string +) + +// Version is the specification version that the package types support. +var Version = semver.Version{ + Major: VersionMajor, + Minor: VersionMinor, + Patch: VersionPatch, + PreRelease: semver.PreRelease(VersionPre), + Metadata: VersionDev, +} From 975ed9dc18d44084a8f1088c1c29c802f05727a4 Mon Sep 17 00:00:00 2001 From: PatWie Date: Mon, 13 May 2019 16:54:32 +0200 Subject: [PATCH 2/2] remove email template directory to further reduce config --- .drone.yml | 4 +-- .infomark.example.yml | 1 - .infomark.yml.ci | 1 - api/app/account.go | 2 +- api/app/auth.go | 2 +- cmd/root.go | 9 ------- email/confirm_email.de.txt | 12 --------- email/confirm_email.en.txt | 12 --------- email/email.go | 41 +++++++++++++++++++++++++++-- email/email.py | 17 ------------ email/request_password_token.de.txt | 9 ------- email/request_password_token.en.txt | 9 ------- symbol/version.go | 2 +- 13 files changed, 44 insertions(+), 77 deletions(-) delete mode 100644 email/confirm_email.de.txt delete mode 100644 email/confirm_email.en.txt delete mode 100644 email/email.py delete mode 100644 email/request_password_token.de.txt delete mode 100644 email/request_password_token.en.txt diff --git a/.drone.yml b/.drone.yml index 3641467..a58d584 100644 --- a/.drone.yml +++ b/.drone.yml @@ -89,7 +89,7 @@ steps: pull: default image: patwie/tar commands: - - tar -czvf infomark-backend.tar.gz api.yaml infomark README.md LICENSE .infomark.example.yml docker-compose.example.yml database/schema.sql email/*.txt files/uploads/ files/generated_files/ files/common/ database/migrations + - tar -czvf infomark-backend.tar.gz api.yaml infomark README.md LICENSE .infomark.example.yml docker-compose.example.yml database/schema.sql files/uploads/ files/generated_files/ files/common/ database/migrations - name: publish_release image: plugins/github-release @@ -126,6 +126,6 @@ services: --- kind: signature -hmac: 79461cde6a81a50bc7e6ad292872f4a10b9126d206196430cd4accaa4f6a61b9 +hmac: 9b5ce91cb6e211a4489fa641cc548efc75721964c9f92fe97d29eab4a410bf92 ... diff --git a/.infomark.example.yml b/.infomark.example.yml index 348c956..a1b64dc 100644 --- a/.infomark.example.yml +++ b/.infomark.example.yml @@ -29,7 +29,6 @@ db_debug: false # email email_from: no-reply@info2.informatik.uni-tuebingen.de -email_templates_dir: /var/www/infomark-staging/app/email sendmail_binary: /usr/sbin/sendmail email_channel_size: 300 diff --git a/.infomark.yml.ci b/.infomark.yml.ci index 141cac2..b1ecd3e 100644 --- a/.infomark.yml.ci +++ b/.infomark.yml.ci @@ -28,7 +28,6 @@ db_debug: false # email email_from: no-reply@info2.informatik.uni-tuebingen.de -email_templates_dir: /drone/src/email sendmail_binary: /usr/sbin/sendmail email_channel_size: 300 diff --git a/api/app/account.go b/api/app/account.go index 82efc14..518c256 100644 --- a/api/app/account.go +++ b/api/app/account.go @@ -113,7 +113,7 @@ func sendConfirmEmailForUser(user *model.User) error { msg, err := email.NewEmailFromTemplate( user.Email, "Confirm Account Instructions", - "confirm_email.en.txt", + email.ConfirmEmailTemplateEN, map[string]string{ "first_name": user.FirstName, "last_name": user.LastName, diff --git a/api/app/auth.go b/api/app/auth.go index e9afa38..a2a8a8c 100644 --- a/api/app/auth.go +++ b/api/app/auth.go @@ -276,7 +276,7 @@ func (rs *AuthResource) RequestPasswordResetHandler(w http.ResponseWriter, r *ht msg, err := email.NewEmailFromTemplate( user.Email, "Password Reset Instructions", - "request_password_token.en.txt", + email.RequestPasswordTokenTemailTemplateEN, map[string]string{ "first_name": user.FirstName, "last_name": user.LastName, diff --git a/cmd/root.go b/cmd/root.go index c363af9..8aaba45 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -91,13 +91,4 @@ func InitConfig() { if err := viper.ReadInConfig(); err != nil { panic(err) } - - // test if config is correct - // verify path to email templates - rootDir := viper.GetString("email_templates_dir") - filename := fmt.Sprintf("%s/%s", rootDir, "request_password_token.en.txt") - if _, err := os.Stat(filename); os.IsNotExist(err) { - fmt.Println("Path from config file .infomark.yml to email templates is wrong!") - panic(err) - } } diff --git a/email/confirm_email.de.txt b/email/confirm_email.de.txt deleted file mode 100644 index ffab436..0000000 --- a/email/confirm_email.de.txt +++ /dev/null @@ -1,12 +0,0 @@ -Hi {{.first_name}} {{.last_name}}! - - -Sie müssen nun Ihre E-Mail-Adresse bestätigen: - - Um sich in unser System einzuloggen und die Lösungen zu den Übungsblättern hochzuladen. - - Passwort zurücksetzen - - Account-Warnungen erhalten - -Bitte verwende den folgenden Link, um deine E-Mail-Adresse zu bestätigen: - -{{.confirm_email_url}}/{{.confirm_email_address}}/{{.confirm_email_token}} - diff --git a/email/confirm_email.en.txt b/email/confirm_email.en.txt deleted file mode 100644 index 35665c6..0000000 --- a/email/confirm_email.en.txt +++ /dev/null @@ -1,12 +0,0 @@ -Hi {{.first_name}} {{.last_name}}! - - -You must now confirm your email address to: - - Log into our system and upload your homework solutions - - Reset your password - - Receive account alerts - -Please use the following link to confirm your email address: - -{{.confirm_email_url}}/{{.confirm_email_address}}/{{.confirm_email_token}} - diff --git a/email/email.go b/email/email.go index f530285..e6dd5fb 100644 --- a/email/email.go +++ b/email/email.go @@ -63,8 +63,8 @@ func NewEmailFromUser(toEmail string, subject string, body string, user *model.U } // NewEmailFromTemplate creates a new email structure filling a template file -func NewEmailFromTemplate(toEmail string, subject string, file string, data map[string]string) (*Email, error) { - body, err := LoadAndFillTemplate(file, data) +func NewEmailFromTemplate(toEmail string, subject string, tpl *template.Template, data map[string]string) (*Email, error) { + body, err := FillTemplate(tpl, data) if err != nil { return nil, err } @@ -190,3 +190,40 @@ func LoadAndFillTemplate(file string, data map[string]string) (string, error) { err = t.Execute(&tpl, data) return tpl.String(), err } + +// FillTemplate loads a template and fills out the placeholders. +func FillTemplate(t *template.Template, data map[string]string) (string, error) { + var tpl bytes.Buffer + err := t.Execute(&tpl, data) + return tpl.String(), err +} + +const ( + confirmEmailTemplateSrcEN = `Hi {{.first_name}} {{.last_name}}! + +You must now confirm your email address to: + - Log into our system and upload your homework solutions + - Reset your password + - Receive account alerts + +Please use the following link to confirm your email address: + +{{.confirm_email_url}}/{{.confirm_email_address}}/{{.confirm_email_token}} + +` + + requestPasswordTokenTemailTemplateSrcEN = `Hi {{.first_name}} {{.last_name}}! + +We got a request to change your password. You can change your password using the following link. + +{{.reset_password_url}}/{{.email_address}}/{{.reset_password_token}} + +If you have not requested the change, you can ignore this mail. + +Your password can only be changed manually by you. + +` +) + +var ConfirmEmailTemplateEN *template.Template = template.Must(template.New("confirmEmailTemplateSrcEN").Parse(confirmEmailTemplateSrcEN)) +var RequestPasswordTokenTemailTemplateEN *template.Template = template.Must(template.New("requestPasswordTokenTemailTemplateSrcEN").Parse(requestPasswordTokenTemailTemplateSrcEN)) diff --git a/email/email.py b/email/email.py deleted file mode 100644 index fa22115..0000000 --- a/email/email.py +++ /dev/null @@ -1,17 +0,0 @@ -import os - - -def sendMail(): - sendmail_location = "/usr/sbin/sendmail" # sendmail location - p = os.popen("%s -t" % sendmail_location, "w") - p.write("From: %s\n" % "no-reply@info2.informatik.uni-tuebingen.de") - p.write("To: %s\n" % "patrick.wieschollek@uni-tuebingen.de") - p.write("Subject: TestSubject\n") - p.write("\n") # blank line separating headers from body - p.write("body of the mail") - status = p.close() - if status != 0: - print("Sendmail exit status", status) - - -sendMail() \ No newline at end of file diff --git a/email/request_password_token.de.txt b/email/request_password_token.de.txt deleted file mode 100644 index 5b55793..0000000 --- a/email/request_password_token.de.txt +++ /dev/null @@ -1,9 +0,0 @@ -Hallo {{.first_name}} {{.last_name}}! - -Es gab eine Anfrage dein Passwort zu ändern. Du kannst dies mittels Klick auf folgendne Link durchführen. - -{{.reset_password_url}}/{{.email_address}}/{{.reset_password_token}} - -Wenn du die Änderung nicht angefragt hast, kannst du diese Mail ruhig ignorieren. - -Dein Passwort kann nur durch dich manuell geändert werden. diff --git a/email/request_password_token.en.txt b/email/request_password_token.en.txt deleted file mode 100644 index 892b79e..0000000 --- a/email/request_password_token.en.txt +++ /dev/null @@ -1,9 +0,0 @@ -Hi {{.first_name}} {{.last_name}}! - -We got a request to change your password. You can change your password using the following link. - -{{.reset_password_url}}/{{.email_address}}/{{.reset_password_token}} - -If you have not requested the change, you can ignore this mail. - -Your password can only be changed manually by you. diff --git a/symbol/version.go b/symbol/version.go index e848417..0eeb1e9 100644 --- a/symbol/version.go +++ b/symbol/version.go @@ -32,7 +32,7 @@ var ( // VersionPatch is for backwards-compatible bug fixes. VersionPatch int64 = 1 // VersionPre indicates prerelease. - VersionPre = "beta" + VersionPre = "beta-1" // VersionDev indicates development branch. Releases will be empty string. VersionDev string )