├── main.css ├── screenshots └── form.jpg ├── model ├── company.go ├── address.go ├── signature.go └── components.go ├── views ├── components │ ├── form.templ │ ├── input.templ │ └── signature.templ ├── icons │ └── linkedin.templ └── pages │ └── index.templ ├── package.json ├── .pre-commit-config.yaml ├── .cz.toml ├── .vscode └── settings.json ├── tailwind.config.js ├── CHANGELOG.md ├── docker-compose.yml ├── .gitignore ├── public └── main.js ├── LICENSE ├── .air.toml ├── go.mod ├── README.md ├── tmp └── build-errors.log ├── main.go └── go.sum /main.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /screenshots/form.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dan6erbond/mail-mark/HEAD/screenshots/form.jpg -------------------------------------------------------------------------------- /model/company.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Company struct { 4 | Name string 5 | URL string 6 | Address Address 7 | } 8 | -------------------------------------------------------------------------------- /model/address.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Address struct { 4 | Street string 5 | Number string 6 | Zip string 7 | Area string 8 | } 9 | -------------------------------------------------------------------------------- /views/components/form.templ: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | templ Form(attrs templ.Attributes) { 4 |
5 | { children... } 6 |
7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "@tailwindcss/typography": "^0.5.15", 4 | "daisyui": "^4.12.14", 5 | "tailwindcss": "^3.4.14" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - hooks: 3 | - id: commitizen 4 | - id: commitizen-branch 5 | stages: 6 | - push 7 | repo: https://github.com/commitizen-tools/commitizen 8 | rev: v3.28.0 9 | -------------------------------------------------------------------------------- /.cz.toml: -------------------------------------------------------------------------------- 1 | [tool.commitizen] 2 | name = "cz_conventional_commits" 3 | tag_format = "v$version" 4 | version_scheme = "semver" 5 | version = "0.1.0" 6 | update_changelog_on_bump = true 7 | major_version_zero = true 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "tailwindCSS.includeLanguages": { 3 | "templ": "html", 4 | }, 5 | "emmet.includeLanguages": { 6 | "templ": "html" 7 | }, 8 | "[templ]": { 9 | "editor.defaultFormatter": "a-h.templ" 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ["views/**/*.templ"], 3 | theme: { 4 | extend: {}, 5 | }, 6 | plugins: [require("@tailwindcss/typography"), require("daisyui")], 7 | daisyui: { 8 | themes: ["light"], 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v0.1.0 (2024-11-10) 2 | 3 | ### Feat 4 | 5 | - :sparkles: update document title, header and environment variable prefix 6 | - :tada: implement signature generator with templ, TailwindCSS and Echo 7 | 8 | ### Refactor 9 | 10 | - :recycle: rename to mail-mark and move components to views instead of views/pages 11 | -------------------------------------------------------------------------------- /model/signature.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "github.com/a-h/templ" 4 | 5 | type Signature struct { 6 | Name string 7 | Role string 8 | Email string 9 | PhoneNumber string 10 | Picture string 11 | LinkedInURL string 12 | Company Company 13 | BrandColor string 14 | Attrs templ.Attributes 15 | } 16 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | minio: 3 | image: quay.io/minio/minio 4 | command: server /data --console-address ":9001" 5 | volumes: 6 | - minio:/data 7 | ports: 8 | - 9005:9000 9 | - 9006:9001 10 | environment: 11 | MINIO_ROOT_USER: minioadmin 12 | MINIO_ROOT_PASSWORD: minioadmin 13 | 14 | volumes: 15 | minio: 16 | -------------------------------------------------------------------------------- /views/icons/linkedin.templ: -------------------------------------------------------------------------------- 1 | package icons 2 | 3 | templ LinkedIn(attrs templ.Attributes) { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | go.work.sum 23 | 24 | # env file 25 | .env 26 | 27 | public/* 28 | !public/main.js 29 | 30 | config.yml 31 | node_modules/ 32 | 33 | *_templ.go 34 | -------------------------------------------------------------------------------- /public/main.js: -------------------------------------------------------------------------------- 1 | function copySignature() { 2 | const signature = document.getElementById("signature"); 3 | 4 | try { 5 | navigator.clipboard.write([ 6 | new ClipboardItem({ 7 | "text/html": new Blob([signature.innerHTML], { type: "text/html" }), 8 | }), 9 | ]); 10 | console.log("Copied to clipboard successfully!"); 11 | } catch (err) { 12 | console.error("Failed to copy: ", err); 13 | } 14 | } 15 | 16 | function copySignatureHTML() { 17 | const signature = document.getElementById("signature"); 18 | 19 | try { 20 | navigator.clipboard.writeText(signature.innerHTML); 21 | console.log("Copied to clipboard successfully!"); 22 | } catch (err) { 23 | console.error("Failed to copy: ", err); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.air.toml: -------------------------------------------------------------------------------- 1 | root = "." 2 | testdata_dir = "testdata" 3 | tmp_dir = "tmp" 4 | 5 | [build] 6 | args_bin = [] 7 | bin = "tmp\\main.exe" 8 | cmd = "templ generate && npx tailwindcss -i ./main.css -o ./public/main.css && go build -o ./tmp/main.exe ." 9 | delay = 1000 10 | exclude_dir = ["public", "tmp", "node_modules"] 11 | exclude_file = [] 12 | exclude_regex = ["_test.go", "_templ.go"] 13 | exclude_unchanged = false 14 | follow_symlink = false 15 | full_bin = "" 16 | include_dir = [] 17 | include_ext = ["go", "tpl", "tmpl", "templ", "html", "css", "js"] 18 | include_file = [] 19 | kill_delay = "0s" 20 | log = "build-errors.log" 21 | poll = false 22 | poll_interval = 0 23 | post_cmd = [] 24 | pre_cmd = [] 25 | rerun = false 26 | rerun_delay = 500 27 | send_interrupt = false 28 | stop_on_error = false 29 | 30 | [color] 31 | app = "" 32 | build = "yellow" 33 | main = "magenta" 34 | runner = "green" 35 | watcher = "cyan" 36 | 37 | [log] 38 | main_only = false 39 | silent = false 40 | time = false 41 | 42 | [misc] 43 | clean_on_exit = false 44 | 45 | [proxy] 46 | app_port = 8000 47 | enabled = true 48 | proxy_port = 8001 49 | 50 | [screen] 51 | clear_on_rebuild = false 52 | keep_scroll = true 53 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Dan6erbond/mail-mark 2 | 3 | go 1.23.2 4 | 5 | require ( 6 | github.com/a-h/templ v0.2.793 7 | github.com/google/uuid v1.6.0 8 | github.com/knadh/koanf/parsers/json v0.1.0 9 | github.com/knadh/koanf/parsers/yaml v0.1.0 10 | github.com/knadh/koanf/providers/confmap v0.1.0 11 | github.com/knadh/koanf/providers/env v1.0.0 12 | github.com/knadh/koanf/providers/file v1.1.2 13 | github.com/knadh/koanf/v2 v2.1.2 14 | github.com/labstack/echo v3.3.10+incompatible 15 | github.com/minio/minio-go/v7 v7.0.80 16 | ) 17 | 18 | require ( 19 | github.com/dustin/go-humanize v1.0.1 // indirect 20 | github.com/fsnotify/fsnotify v1.7.0 // indirect 21 | github.com/go-ini/ini v1.67.0 // indirect 22 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 23 | github.com/goccy/go-json v0.10.3 // indirect 24 | github.com/klauspost/compress v1.17.11 // indirect 25 | github.com/klauspost/cpuid/v2 v2.2.8 // indirect 26 | github.com/knadh/koanf/maps v0.1.1 // indirect 27 | github.com/labstack/gommon v0.4.2 // indirect 28 | github.com/mattn/go-colorable v0.1.13 // indirect 29 | github.com/mattn/go-isatty v0.0.20 // indirect 30 | github.com/minio/md5-simd v1.1.2 // indirect 31 | github.com/mitchellh/copystructure v1.2.0 // indirect 32 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 33 | github.com/rs/xid v1.6.0 // indirect 34 | github.com/valyala/bytebufferpool v1.0.0 // indirect 35 | github.com/valyala/fasttemplate v1.2.2 // indirect 36 | golang.org/x/crypto v0.29.0 // indirect 37 | golang.org/x/net v0.30.0 // indirect 38 | golang.org/x/sys v0.27.0 // indirect 39 | golang.org/x/text v0.20.0 // indirect 40 | gopkg.in/yaml.v3 v3.0.1 // indirect 41 | ) 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GoMailMark 2 | 3 | A Go web application to generate email signatures and host uploaded images with S3. 4 | 5 | GoMailMark allows users to enter information about them, uploads given images to S3, and generates an HTML signature using [Templ](https://templ.guide/). 6 | 7 | ## Features 8 | 9 | - [x] Generate email signatures based on a [Templ](https://templ.guide/). 10 | - [x] Upload image files to S3. 11 | - [x] 12-factor configuration. 12 | - [ ] Easy deployment with Docker. 13 | 14 | ## Screenshots 15 | 16 | !["GoMailMark"](./screenshots/form.jpg) 17 | 18 | ## Usage 19 | 20 | To use GoMailMark you'll need to create an S3-compatible bucket to which images will be uploaded. MinIO is supported. 21 | 22 | GoMailMark runs a Go server and requires some configuration beforehand via environment variables pre-fixed with `MAIL_MARK_` or a `config.json`/`config.yml` in the current working directory. 23 | 24 | To start the server run `go run main.go` and head to [`localhost:8000`](http://localhost:8000). The default port can be configured via the configuration key `server.port`. 25 | 26 | If you want to programmatically use GoMailMark, you can send HTTP POST requests to `/` with a multi-part form data containing the following fields: 27 | 28 | 1. `name` - Your name. 29 | 2. `email` - Your email address. 30 | 3. `email` - Your email address. 31 | 4. `phone` - Your phone number. 32 | 5. `linkedin` - Your LinkedIn URL. 33 | 6. `picture` - A file containing an image to be uploaded to S3. 34 | 35 | GoMailMark is designed to be forked and updated to your needs. The fields in the signature as well as its overall design can be easily modified. GoMailMark simply provides a web server and the S3 upload functionality for convenience. 36 | 37 | ## Configuration 38 | 39 | GoMailMark can be configured using various configuration formats, for example your `config.yml` could look like this: 40 | 41 | ```yml 42 | s3: 43 | host: localhost:9005 44 | accessKeyId: minioadmin 45 | secretAccessKey: minioadmin 46 | bucket: email-signature-generator 47 | signature: 48 | company: 49 | name: Your Company Name 50 | url: https://example.com 51 | address: 52 | street: Street 53 | number: Nr 54 | zip: Zip 55 | area: Area 56 | brandColor: "#hexCode" 57 | ``` 58 | 59 | GoMailMark will use these variables to connect to S3 and provide default values to the template for your brand. 60 | -------------------------------------------------------------------------------- /views/pages/index.templ: -------------------------------------------------------------------------------- 1 | package pages 2 | 3 | import ( 4 | "github.com/Dan6erbond/mail-mark/model" 5 | "github.com/Dan6erbond/mail-mark/views/components" 6 | ) 7 | 8 | templ Home() { 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | GoMailMark 18 | 19 | 20 |
21 |
22 |

GoMailMark

23 | @components.Form(templ.Attributes{ 24 | "action": "/", 25 | "method": "post", 26 | "hx-boost": "true", 27 | "hx-target": "#signature", 28 | "hx-encoding": "multipart/form-data", 29 | }) { 30 | @components.Input(model.Input{ 31 | Label: "Name", 32 | Name: "name", 33 | Placeholder: "Your name...", 34 | }) 35 | @components.Input(model.Input{ 36 | Label: "Role", 37 | Name: "role", 38 | Placeholder: "Your role...", 39 | }) 40 | @components.Input(model.Input{ 41 | Label: "Email", 42 | Name: "email", 43 | Type: "email", 44 | Placeholder: "Your email...", 45 | }) 46 | @components.Input(model.Input{ 47 | Label: "Phone number", 48 | Name: "phone", 49 | Type: "tel", 50 | Placeholder: "Your phone number...", 51 | Attrs: templ.Attributes{ 52 | "pattern": "\\+41 [0-9]{2} [0-9]{3} [0-9]{2} [0-9]{2}", 53 | }, 54 | }) 55 |

Phone number must follow format +41 XX XXX XX XX

56 | @components.Input(model.Input{ 57 | Label: "LinkedIn", 58 | Name: "linkedin", 59 | Type: "url", 60 | Placeholder: "Your LinkedIn URL...", 61 | }) 62 | @components.FileInput(model.Input{ 63 | Label: "Picture", 64 | Name: "picture", 65 | }) 66 | 67 | } 68 |
69 |
70 | 71 | 72 |
73 |
74 |
75 | 76 | 77 | } 78 | -------------------------------------------------------------------------------- /tmp/build-errors.log: -------------------------------------------------------------------------------- 1 | exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 0xc000013aexit status 0xc000013aexit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1 -------------------------------------------------------------------------------- /views/components/input.templ: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "fmt" 5 | "github.com/Dan6erbond/mail-mark/model" 6 | ) 7 | 8 | templ Input(input model.Input) { 9 |
15 | 87 |
88 | } 89 | 90 | templ FileInput(input model.Input) { 91 |
96 | 140 |
141 | } 142 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "log/slog" 7 | "net/http" 8 | "net/url" 9 | "strings" 10 | 11 | "github.com/Dan6erbond/mail-mark/model" 12 | "github.com/Dan6erbond/mail-mark/views/components" 13 | "github.com/Dan6erbond/mail-mark/views/pages" 14 | "github.com/a-h/templ" 15 | "github.com/google/uuid" 16 | "github.com/labstack/echo" 17 | "github.com/minio/minio-go/v7" 18 | "github.com/minio/minio-go/v7/pkg/credentials" 19 | 20 | "github.com/knadh/koanf/parsers/json" 21 | "github.com/knadh/koanf/parsers/yaml" 22 | "github.com/knadh/koanf/providers/confmap" 23 | "github.com/knadh/koanf/providers/env" 24 | "github.com/knadh/koanf/providers/file" 25 | 26 | "github.com/knadh/koanf/v2" 27 | ) 28 | 29 | var k = koanf.New(".") 30 | 31 | func main() { 32 | k.Load(confmap.Provider(map[string]any{ 33 | "server": map[string]any{ 34 | "port": 8000, 35 | }, 36 | }, ""), nil) 37 | 38 | k.Load(file.Provider("config.json"), json.Parser()) 39 | k.Load(file.Provider("config.yml"), yaml.Parser()) 40 | 41 | k.Load(env.Provider("MAIL_MARK_", ".", func(s string) string { 42 | return strings.Replace(strings.ToLower( 43 | strings.TrimPrefix(s, "MAIL_MARK_")), "_", ".", -1) 44 | }), nil) 45 | 46 | minioClient, err := minio.New(k.String("s3.host"), &minio.Options{ 47 | Creds: credentials.NewStaticV4(k.String("s3.accessKeyId"), k.String("s3.secretAccessKey"), ""), 48 | Secure: false, 49 | }) 50 | 51 | if err != nil { 52 | log.Fatalln(err) 53 | } 54 | 55 | e := echo.New() 56 | 57 | e.Static("/public", "public") 58 | 59 | e.GET("/", func(c echo.Context) error { 60 | buf := templ.GetBuffer() 61 | defer templ.ReleaseBuffer(buf) 62 | 63 | home := pages.Home() 64 | if err := home.Render(c.Request().Context(), buf); err != nil { 65 | return err 66 | } 67 | 68 | return c.HTML(http.StatusOK, buf.String()) 69 | }) 70 | 71 | e.POST("/", func(c echo.Context) error { 72 | buf := templ.GetBuffer() 73 | defer templ.ReleaseBuffer(buf) 74 | 75 | signatureModel := model.Signature{ 76 | Name: c.FormValue("name"), 77 | Role: c.FormValue("role"), 78 | Email: c.FormValue("email"), 79 | PhoneNumber: c.FormValue("phone"), 80 | LinkedInURL: c.FormValue("linkedin"), 81 | Company: model.Company{ 82 | Name: k.String("signature.company.name"), 83 | URL: k.String("signature.company.url"), 84 | Address: model.Address{ 85 | Street: k.String("signature.company.address.street"), 86 | Number: k.String("signature.company.address.number"), 87 | Zip: k.String("signature.company.address.zip"), 88 | Area: k.String("signature.company.address.area"), 89 | }, 90 | }, 91 | BrandColor: k.String("signature.brandColor"), 92 | } 93 | 94 | picture, err := c.FormFile("picture") 95 | if err != nil && err.Error() != "http: no such file" { 96 | slog.Error("Error reading picture", slog.Attr{Key: "error", Value: slog.AnyValue(err)}) 97 | return err 98 | } 99 | 100 | var pictureURL *url.URL 101 | 102 | if picture != nil { 103 | src, err := picture.Open() 104 | if err != nil { 105 | slog.Error("Error opening picture") 106 | return err 107 | } 108 | 109 | contentType := "application/octet-stream" 110 | 111 | id := uuid.New() 112 | 113 | // Upload the test file with FPutObject 114 | info, err := minioClient.PutObject(c.Request().Context(), k.String("s3.bucket"), id.String(), src, -1, minio.PutObjectOptions{ContentType: contentType}) 115 | if err != nil { 116 | slog.Error("Error uploading picture", slog.Attr{Key: "error", Value: slog.AnyValue(err)}) 117 | } 118 | 119 | // Set request parameters 120 | reqParams := make(url.Values) 121 | reqParams.Set("response-content-disposition", "attachment; filename=\"your-filename.txt\"") 122 | 123 | pictureURL = &url.URL{ 124 | Host: k.String("s3.host"), 125 | } 126 | pictureURL.Scheme = "http" 127 | pictureURL.Path = fmt.Sprintf("/%s/%s", k.String("s3.bucket"), info.Key) 128 | 129 | signatureModel.Picture = pictureURL.String() 130 | } 131 | 132 | if err := components.Signature(signatureModel).Render(c.Request().Context(), buf); err != nil { 133 | return err 134 | } 135 | 136 | return c.HTML(http.StatusOK, buf.String()) 137 | }) 138 | 139 | err = e.Start(fmt.Sprintf(":%d", k.Int("server.port"))) 140 | 141 | if err != nil { 142 | log.Fatalln("Error launching server", err) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /model/components.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/a-h/templ" 5 | ) 6 | 7 | type Anchor struct { 8 | Href string 9 | Label string 10 | Icon templ.Component 11 | Attrs templ.Attributes 12 | } 13 | 14 | type Banner struct { 15 | Title templ.Component 16 | Description string 17 | CallToAction Button 18 | SecondaryCallToAction Button 19 | } 20 | 21 | type Button struct { 22 | Label string 23 | Attrs templ.Attributes 24 | } 25 | 26 | type Card struct { 27 | Title string 28 | Content string 29 | Source string 30 | Alt string 31 | } 32 | 33 | type Chat struct { 34 | Messages []ChatMessage 35 | } 36 | 37 | type ChatMessage struct { 38 | AvatarURL string 39 | Sender string 40 | Time string 41 | Message string 42 | Footer string 43 | Location string 44 | Classes string 45 | } 46 | 47 | type Checkbox struct { 48 | ID string 49 | Label string 50 | Name string 51 | Checked bool 52 | Class string 53 | Attrs templ.Attributes 54 | } 55 | 56 | type Combobox struct { 57 | Label string 58 | Name string 59 | URL string 60 | Options []string 61 | Selected []string 62 | } 63 | 64 | type CompanyInfo struct { 65 | Icon templ.Component 66 | Name string 67 | Description string 68 | Copyright string 69 | } 70 | 71 | type DropdownItem struct { 72 | Label string 73 | Attrs templ.Attributes 74 | } 75 | 76 | type Feature struct { 77 | Icon templ.Component 78 | Title string 79 | Description string 80 | URL string 81 | } 82 | 83 | type Image struct { 84 | Source string 85 | Alt string 86 | } 87 | 88 | type Input struct { 89 | ID string 90 | Type string // defaults to "text" 91 | Label string 92 | Name string 93 | Value string 94 | Placeholder string 95 | Err string 96 | Attrs templ.Attributes 97 | Classes string 98 | Icon templ.Component 99 | Disabled bool 100 | DisabledMessage string 101 | } 102 | 103 | type PaginationItem struct { 104 | URL string 105 | Page int 106 | Low int 107 | High int 108 | MaxPages int 109 | } 110 | 111 | type Price struct { 112 | Title string 113 | Description string 114 | Price string 115 | Per string 116 | IncludedFeatures []string 117 | ExcludedFeatures []string 118 | CallToAction Button 119 | Footer templ.Component 120 | } 121 | 122 | type Range struct { 123 | ID string 124 | Label string 125 | Name string 126 | Value int 127 | Min int 128 | Max int 129 | Step int 130 | Class string 131 | } 132 | 133 | type Rating struct { 134 | Name string 135 | Min int 136 | Max int 137 | Class string 138 | Value int 139 | } 140 | 141 | type Script struct { 142 | Source string 143 | Defer bool 144 | } 145 | 146 | type Select struct { 147 | ID string 148 | Label string 149 | Name string 150 | Options []SelectOption 151 | Attrs templ.Attributes 152 | } 153 | 154 | type SelectOption struct { 155 | Label string 156 | Value string 157 | Selected bool 158 | Disabled bool 159 | } 160 | 161 | type Stat struct { 162 | Title string 163 | Value string 164 | Description string 165 | } 166 | 167 | type Status struct { 168 | Code int 169 | Title string 170 | Description string 171 | ReturnButton Button 172 | } 173 | 174 | type Tab struct { 175 | Label string 176 | Content templ.Component 177 | } 178 | 179 | type Testimonial struct { 180 | Avatar templ.Component 181 | Name string 182 | Rating int 183 | Content string 184 | } 185 | 186 | type Textarea struct { 187 | ID string 188 | Label string 189 | Name string 190 | Placeholder string 191 | Value string 192 | Rows int 193 | Err string 194 | Class string 195 | Attrs templ.Attributes 196 | } 197 | 198 | type TimelineItem struct { 199 | Start string 200 | Middle templ.Component 201 | End string 202 | } 203 | 204 | type Toast struct { 205 | Name string 206 | ToastClasses string 207 | AlertClasses string 208 | } 209 | 210 | type Toggle struct { 211 | ID string 212 | Before string 213 | After string 214 | Name string 215 | Checked bool 216 | Class string 217 | Highlight bool 218 | Attrs templ.Attributes 219 | } 220 | -------------------------------------------------------------------------------- /views/components/signature.templ: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "github.com/Dan6erbond/mail-mark/model" 5 | "github.com/Dan6erbond/mail-mark/views/icons" 6 | ) 7 | 8 | func styles(style string) templ.Attributes { 9 | return templ.Attributes{"style": style} 10 | } 11 | 12 | templ Signature(signature model.Signature) { 13 | 14 | 15 | 16 | 17 | 18 | 19 | 31 | 53 | 54 | 55 | 60 | 61 | 62 | 72 | 87 | 88 | 89 |
20 |

21 | { signature.Name } 22 |
23 | { signature.Role } 24 |

25 | 26 | 27 | { 28 | 29 | 30 |
32 | 33 | E: 34 | 35 | { signature.Email } 36 | 37 |
38 |
39 | 40 | M: 41 | 42 | { signature.PhoneNumber } 43 | 44 |
45 |
46 | A: 47 | { signature.Company.Name }, 48 | 49 | Überlandstrasse 1, 50 | 8600 Dübendorf 51 | 52 |
56 | 57 | Banner 58 | 59 |
63 | 69 | www.innopeak.ch 70 | 71 | 73 | 74 | 75 | 76 | 83 | 84 | 85 |
77 | 78 | @icons.LinkedIn(templ.Attributes{ 79 | "style": "color: " + signature.BrandColor + "", 80 | }) 81 | 82 |
86 |
90 | } 91 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/a-h/templ v0.2.793 h1:Io+/ocnfGWYO4VHdR0zBbf39PQlnzVCVVD+wEEs6/qY= 2 | github.com/a-h/templ v0.2.793/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 6 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 7 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 8 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 9 | github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= 10 | github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= 11 | github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= 12 | github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 13 | github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= 14 | github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 15 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 16 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 17 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 18 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 19 | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= 20 | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 21 | github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 22 | github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= 23 | github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 24 | github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= 25 | github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= 26 | github.com/knadh/koanf/parsers/json v0.1.0 h1:dzSZl5pf5bBcW0Acnu20Djleto19T0CfHcvZ14NJ6fU= 27 | github.com/knadh/koanf/parsers/json v0.1.0/go.mod h1:ll2/MlXcZ2BfXD6YJcjVFzhG9P0TdJ207aIBKQhV2hY= 28 | github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w= 29 | github.com/knadh/koanf/parsers/yaml v0.1.0/go.mod h1:cvbUDC7AL23pImuQP0oRw/hPuccrNBS2bps8asS0CwY= 30 | github.com/knadh/koanf/providers/confmap v0.1.0 h1:gOkxhHkemwG4LezxxN8DMOFopOPghxRVp7JbIvdvqzU= 31 | github.com/knadh/koanf/providers/confmap v0.1.0/go.mod h1:2uLhxQzJnyHKfxG927awZC7+fyHFdQkd697K4MdLnIU= 32 | github.com/knadh/koanf/providers/env v1.0.0 h1:ufePaI9BnWH+ajuxGGiJ8pdTG0uLEUWC7/HDDPGLah0= 33 | github.com/knadh/koanf/providers/env v1.0.0/go.mod h1:mzFyRZueYhb37oPmC1HAv/oGEEuyvJDA98r3XAa8Gak= 34 | github.com/knadh/koanf/providers/file v1.1.2 h1:aCC36YGOgV5lTtAFz2qkgtWdeQsgfxUkxDOe+2nQY3w= 35 | github.com/knadh/koanf/providers/file v1.1.2/go.mod h1:/faSBcv2mxPVjFrXck95qeoyoZ5myJ6uxN8OOVNJJCI= 36 | github.com/knadh/koanf/v2 v2.1.2 h1:I2rtLRqXRy1p01m/utEtpZSSA6dcJbgGVuE27kW2PzQ= 37 | github.com/knadh/koanf/v2 v2.1.2/go.mod h1:Gphfaen0q1Fc1HTgJgSTC4oRX9R2R5ErYMZJy8fLJBo= 38 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 39 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 40 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 41 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 42 | github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg= 43 | github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s= 44 | github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= 45 | github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= 46 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 47 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 48 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 49 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 50 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 51 | github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= 52 | github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= 53 | github.com/minio/minio-go/v7 v7.0.80 h1:2mdUHXEykRdY/BigLt3Iuu1otL0JTogT0Nmltg0wujk= 54 | github.com/minio/minio-go/v7 v7.0.80/go.mod h1:84gmIilaX4zcvAWWzJ5Z1WI5axN+hAbM5w25xf8xvC0= 55 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 56 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 57 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 58 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 59 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 60 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 61 | github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= 62 | github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= 63 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 64 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 65 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 66 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 67 | github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= 68 | github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 69 | golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= 70 | golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= 71 | golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= 72 | golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= 73 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 74 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 75 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 76 | golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= 77 | golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 78 | golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= 79 | golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= 80 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 81 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 82 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 83 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 84 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 85 | --------------------------------------------------------------------------------