├── .github
├── ISSUE_TEMPLATE
│ └── bug_report.md
└── workflows
│ └── main.yml
├── .gitignore
├── .golangci.yml
├── LICENSE
├── Makefile
├── README.md
├── core.go
├── core_test.go
├── data_api.go
├── example_test.go
├── fixtures
└── fixtures.go
├── go.mod
├── http_client.go
├── http_client_decl.go
├── http_client_mock.go
├── mailjet_client.go
├── mailjet_client_decl.go
├── mailjet_client_test.go
├── mailjet_resources.go
├── resources
├── resources.go
└── time_test.go
├── smtp_client.go
├── smtp_client_decl.go
├── smtp_client_mock.go
└── tests
└── main.go
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: 'bug'
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Code snippet(preferably) or steps to reproduce the behavior:
15 | 1. Do this
16 | 2. Then do that
17 |
18 | **Expected behavior**
19 | A clear and concise description of what you expected to happen.
20 |
21 | **Go version**
22 | Specify the go version.
23 | Since the Go release is supported until there are two newer major releases, there is no guarantee that the bug will be fixed for older Go versions(https://go.dev/doc/devel/release#policy).
24 |
25 | **mailjet-apiv3-go release**
26 | Specify mailjet-apiv3-go release version.
27 | It should be the latest from https://github.com/mailjet/mailjet-apiv3-go/releases. If not, update to the latest and try to reproduce the bug.
28 |
29 | **Additional context**
30 | Please feel free to add any other context about the problem here.
31 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | pull_request:
5 | branches: [ master, main ]
6 | push:
7 | branches: [ master, main ]
8 |
9 | jobs:
10 | test:
11 | name: test
12 | strategy:
13 | matrix:
14 | go-version:
15 | - 1.22.x
16 | - 1.23.x
17 | os: [ ubuntu-latest ]
18 | runs-on: ${{ matrix.os }}
19 | steps:
20 | - name: Checkout
21 | uses: actions/checkout@v4
22 | with:
23 | fetch-depth: 0 # for golangci-lint's -new-from-rev
24 |
25 | - name: Set up Go
26 | uses: actions/setup-go@v5
27 | with:
28 | go-version: ${{ matrix.go-version }}
29 | cache: true # caching and restoring go modules and build outputs
30 |
31 | - run: go env
32 |
33 | - name: Install deps
34 | run: go mod download
35 |
36 | - name: nilaway
37 | run: make nilaway
38 |
39 | - name: lint
40 | run: make lint
41 |
42 | - name: Test
43 | run: go test ./... -race -count=1
44 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled Object files, Static and Dynamic libs (Shared Objects)
2 | .idea/
3 | *.o
4 | *.a
5 | *.so
6 |
7 | *.exe
8 | *.test
9 | *.prof
10 |
11 | # MacOS
12 | /.DS_Store
13 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | linters:
2 | # Please, do not use `enable-all`: it's deprecated and will be removed soon.
3 | # Inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint.
4 | # Full list of linters - https://golangci-lint.run/usage/linters
5 | disable-all: true
6 | enable:
7 | - typecheck
8 | - errcheck # Mandatory. Do not disable.
9 | - ineffassign # Mandatory. Do not disable.
10 | - staticcheck # Mandatory. Do not disable.
11 | - govet
12 | - gosimple
13 | - gosec
14 | - bodyclose # https://github.com/timakin/bodyclose
15 | - goimports
16 | - stylecheck
17 | - gocritic
18 | - gomodguard
19 | - nolintlint
20 |
21 | # TODO:
22 | # - unused
23 |
24 | # TODO(v5): enable
25 | # - noctx
26 |
27 | linters-settings:
28 | errcheck:
29 | # List of functions to exclude from checking, where each entry is a single function to exclude.
30 | # See https://github.com/kisielk/errcheck#excluding-functions for details.
31 | exclude-functions:
32 | - (io.Closer).Close
33 | - (io.ReadCloser).Close
34 |
35 | govet:
36 | enable-all: true
37 | disable:
38 | - shadow
39 | - fieldalignment
40 |
41 | staticcheck:
42 | # SAxxxx checks in https://staticcheck.dev/docs/configuration/options/#checks
43 | # Example (to disable some checks): [ "all", "-SA1000", "-SA1001"]
44 | # Default: ["*"]
45 | # TODO(Go1.20+): enable SA1019
46 | checks: ["all", "-SA1019"]
47 |
48 | stylecheck:
49 | # https://staticcheck.io/docs/options#checks
50 | # TODO(v5): enable all:
51 | checks: ["all", "-ST1003", "-ST1005"]
52 |
53 | gomodguard:
54 | blocked:
55 | # List of blocked modules.
56 | modules:
57 | - github.com/pkg/errors:
58 | recommendations:
59 | - errors
60 | - github.com/mailgun/errors
61 | reason: "Deprecated"
62 |
63 | issues:
64 | # Maximum issues count per one linter. Set to 0 to disable. Default is 50.
65 | max-issues-per-linter: 0
66 |
67 | # Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
68 | max-same-issues: 50
69 |
70 | run:
71 | # include test files or not, default is true
72 | tests: true
73 |
74 | # Timeout for analysis, e.g. 30s, 5m.
75 | # Default: 1m
76 | timeout: 5m
77 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Mailjet
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 |
23 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .DEFAULT_GOAL := all
2 |
3 | PACKAGE := github.com/mailjet/mailjet-apiv3-go
4 | GOPATH=$(shell go env GOPATH)
5 |
6 | NILAWAY = $(GOPATH)/bin/nilaway
7 | $(NILAWAY):
8 | go install go.uber.org/nilaway/cmd/nilaway@latest
9 |
10 | .PHONY: all
11 | all: test
12 |
13 | .PHONY: test
14 | test:
15 | go test . -race -count=1
16 |
17 | .PHONY: nilaway
18 | nilaway: $(NILAWAY)
19 | $(NILAWAY) -include-pkgs="$(PACKAGE)" -test=false ./...
20 |
21 | # linter:
22 | GOLINT = $(GOPATH)/bin/golangci-lint
23 | $(GOLINT):
24 | curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOPATH)/bin v1.61.0
25 |
26 | .PHONY: lint
27 | lint: $(GOLINT)
28 | $(GOLINT) run
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [mailjet]: http://www.mailjet.com
2 | [api_credential]: https://app.mailjet.com/account/api_keys
3 | [doc]: http://dev.mailjet.com/guides/?go#
4 |
5 |
6 | 
7 |
8 | # Official Mailjet Go Client
9 |
10 | [](https://github.com/mailjet/mailjet-apiv3-go/actions/workflows/main.yml)
11 | [](https://pkg.go.dev/github.com/mailjet/mailjet-apiv3-go/v4)
12 | [](https://goreportcard.com/report/github.com/mailjet/mailjet-apiv3-go/v4)
13 |
14 | ## Overview
15 |
16 | This repository contains the official Go wrapper for the Mailjet API.
17 |
18 | Check out all the resources and in the [Official Documentation][doc].
19 |
20 | ## Table of contents
21 |
22 | - [Compatibility](#compatibility)
23 | - [Installation](#installation)
24 | - [Authentication](#authentication)
25 | - [Functional test](#functional-test)
26 | - [Make your first call](#make-your-first-call)
27 | - [Client / Call configuration specifics](#client--call-configuration-specifics)
28 | - [Send emails through proxy](#send-emails-through-proxy)
29 | - [Request examples](#request-examples)
30 | - [POST request](#post-request)
31 | - [Simple POST request](#simple-post-request)
32 | - [Using actions](#using-actions)
33 | - [GET request](#get-request)
34 | - [Retrieve all objects](#retrieve-all-objects)
35 | - [Use filtering](#use-filtering)
36 | - [Retrieve a single object](#retrieve-a-single-object)
37 | - [PUT request](#put-request)
38 | - [DELETE request](#delete-request)
39 | - [Contribute](#contribute)
40 |
41 | ## Compatibility
42 |
43 | Our library requires Go version 1.13 or higher.
44 | But since [each major Go release is supported until there are two newer major releases](https://go.dev/doc/devel/release#policy), there is no guarantee that it will be working on unsupported Go versions.
45 |
46 | **NOTE: Backward compatibility has been broken with the `v3.0` release which includes versioned paths required by go modules (See [Releasing Modules](https://github.com/golang/go/wiki/Modules#releasing-modules-v2-or-higher)).**
47 |
48 | ### Installation
49 |
50 | Get package:
51 |
52 | ```
53 | go get github.com/mailjet/mailjet-apiv3-go/v4
54 | ```
55 |
56 | And import the Mailjet wrapper:
57 |
58 | ```go
59 | import (
60 | "github.com/mailjet/mailjet-apiv3-go/v4"
61 | "github.com/mailjet/mailjet-apiv3-go/v4/resources"
62 | )
63 | ```
64 |
65 | ## Authentication
66 |
67 | The Mailjet Email API uses your API and Secret keys for authentication. [Grab][api_credential] and save your Mailjet API credentials.
68 |
69 | ```bash
70 | export MJ_APIKEY_PUBLIC='your API key'
71 | export MJ_APIKEY_PRIVATE='your API secret'
72 | ```
73 |
74 | Then initialize your Mailjet client:
75 |
76 | ```go
77 | // Get your environment Mailjet keys and connect
78 | publicKey := os.Getenv("MJ_APIKEY_PUBLIC")
79 | secretKey := os.Getenv("MJ_APIKEY_PRIVATE")
80 |
81 | mj := mailjet.NewMailjetClient(publicKey, secretKey)
82 | ```
83 |
84 | ### Functional test
85 |
86 | In the `tests` folder you will find a small program using the wrapper. It can be used to check whether the Mailjet API keys in your environment are valid and active.
87 |
88 | ```
89 | go run main.go
90 | ```
91 |
92 | ## Make your first call
93 |
94 | Here's an example on how to send an email:
95 |
96 | ```go
97 | package main
98 |
99 | import (
100 | "fmt"
101 | "log"
102 | "os"
103 |
104 | "github.com/mailjet/mailjet-apiv3-go/v4"
105 | )
106 |
107 | func main() {
108 | mailjetClient := mailjet.NewMailjetClient(os.Getenv("MJ_APIKEY_PUBLIC"), os.Getenv("MJ_APIKEY_PRIVATE"))
109 | messagesInfo := []mailjet.InfoMessagesV31{
110 | {
111 | From: &mailjet.RecipientV31{
112 | Email: "pilot@mailjet.com",
113 | Name: "Mailjet Pilot",
114 | },
115 | To: &mailjet.RecipientsV31{
116 | mailjet.RecipientV31{
117 | Email: "passenger1@mailjet.com",
118 | Name: "passenger 1",
119 | },
120 | },
121 | Subject: "Your email flight plan!",
122 | TextPart: "Dear passenger 1, welcome to Mailjet! May the delivery force be with you!",
123 | HTMLPart: "
Dear passenger 1, welcome to Mailjet!
May the delivery force be with you!",
124 | },
125 | }
126 | messages := mailjet.MessagesV31{Info: messagesInfo}
127 | res, err := mailjetClient.SendMailV31(&messages)
128 | if err != nil {
129 | log.Fatal(err)
130 | }
131 | fmt.Printf("Data: %+v\n", res)
132 | }
133 | ```
134 |
135 | ## Client / Call configuration specifics
136 |
137 | ### Base URL
138 |
139 | The default base domain name for the Mailjet API is `https://api.mailjet.com`. You can modify this base URL by adding a different URL in the client configuration for your call:
140 |
141 | ```go
142 | mailjetClient := NewMailjetClient(os.Getenv("MJ_APIKEY_PUBLIC"), os.Getenv("MJ_APIKEY_PRIVATE"), "https://api.us.mailjet.com")
143 | ```
144 |
145 | If your account has been moved to Mailjet's **US architecture**, the URL value you need to set is `https://api.us.mailjet.com`.
146 |
147 | ### Send emails through proxy
148 |
149 | ```go
150 | package main
151 |
152 | import (
153 | "fmt"
154 | "log"
155 | "net/http"
156 | "net/url"
157 | "os"
158 |
159 | "github.com/mailjet/mailjet-apiv3-go/v4"
160 | )
161 |
162 | // Set the http client with the given proxy url
163 | func setupProxy(proxyURLStr string) *http.Client {
164 | proxyURL, err := url.Parse(proxyURLStr)
165 | if err != nil {
166 | log.Fatal(err)
167 | }
168 | tr := &http.Transport{Proxy: http.ProxyURL(proxyURL)}
169 | client := &http.Client{}
170 | client.Transport = tr
171 |
172 | return client
173 | }
174 |
175 | func main() {
176 | publicKey := os.Getenv("MJ_APIKEY_PUBLIC")
177 | secretKey := os.Getenv("MJ_APIKEY_PRIVATE")
178 | proxyURL := os.Getenv("HTTP_PROXY")
179 |
180 | mj := mailjet.NewMailjetClient(publicKey, secretKey)
181 |
182 | // Here we inject our http client configured with our proxy
183 | client := setupProxy(proxyURL)
184 | mj.SetClient(client)
185 |
186 | messagesInfo := []mailjet.InfoMessagesV31{
187 | {
188 | From: &mailjet.RecipientV31{
189 | Email: "qwe@qwe.com",
190 | Name: "Bob Patrick",
191 | },
192 | To: &mailjet.RecipientsV31{
193 | mailjet.RecipientV31{
194 | Email: "qwe@qwe.com",
195 | },
196 | },
197 | Subject: "Hello World!",
198 | TextPart: "Hi there !",
199 | },
200 | }
201 |
202 | messages := &mailjet.MessagesV31{Info: messagesInfo}
203 |
204 | res, err := mj.SendMailV31(messages)
205 | if err != nil {
206 | fmt.Println(err)
207 | } else {
208 | fmt.Println("Success")
209 | fmt.Println(res)
210 | }
211 | }
212 | ```
213 |
214 | ## Request examples
215 |
216 | ### POST request
217 |
218 | #### Simple POST request
219 |
220 | ```go
221 | // Create a new contact.
222 | package main
223 |
224 | import (
225 | "fmt"
226 | "os"
227 |
228 | "github.com/mailjet/mailjet-apiv3-go/v4"
229 | "github.com/mailjet/mailjet-apiv3-go/v4/resources"
230 | )
231 |
232 | func main() {
233 | mailjetClient := mailjet.NewMailjetClient(os.Getenv("MJ_APIKEY_PUBLIC"), os.Getenv("MJ_APIKEY_PRIVATE"))
234 | var data []resources.Contact
235 | mr := &mailjet.Request{
236 | Resource: "contact",
237 | }
238 | fmr := &mailjet.FullRequest{
239 | Info: mr,
240 | Payload: &resources.Contact{
241 | Email: "passenger@mailjet.com",
242 | IsExcludedFromCampaigns: true,
243 | Name: "New Contact",
244 | },
245 | }
246 | err := mailjetClient.Post(fmr, &data)
247 | if err != nil {
248 | fmt.Println(err)
249 | }
250 | fmt.Printf("Data array: %+v\n", data)
251 | }
252 | ```
253 |
254 | #### Using actions
255 |
256 | ```go
257 | // Create : Manage a contact subscription to a list
258 | package main
259 |
260 | import (
261 | "fmt"
262 | "os"
263 |
264 | "github.com/mailjet/mailjet-apiv3-go/v4"
265 | "github.com/mailjet/mailjet-apiv3-go/v4/resources"
266 | )
267 |
268 | func main() {
269 | mailjetClient := mailjet.NewMailjetClient(os.Getenv("MJ_APIKEY_PUBLIC"), os.Getenv("MJ_APIKEY_PRIVATE"))
270 | var data []resources.ContactManagecontactslists
271 | mr := &mailjet.Request{
272 | Resource: "contact",
273 | ID: 423, // replace with your contact ID here
274 | Action: "managecontactslists",
275 | }
276 | fmr := &mailjet.FullRequest{
277 | Info: mr,
278 | Payload: &resources.ContactManagecontactslists{
279 | ContactsLists: []resources.ContactsListAction{ // replace with your contact lists here
280 | {
281 | ListID: 432,
282 | Action: "addnoforce",
283 | },
284 | {
285 | ListID: 553,
286 | Action: "addforce",
287 | },
288 | },
289 | },
290 | }
291 | err := mailjetClient.Post(fmr, &data)
292 | if err != nil {
293 | fmt.Println(err)
294 | }
295 | fmt.Printf("Data array: %+v\n", data)
296 | }
297 | ```
298 |
299 | ### GET request
300 |
301 | #### Retrieve all objects
302 |
303 | ```go
304 | // Retrieve all contacts:
305 | package main
306 |
307 | import (
308 | "fmt"
309 | "os"
310 |
311 | "github.com/mailjet/mailjet-apiv3-go/v4"
312 | "github.com/mailjet/mailjet-apiv3-go/v4/resources"
313 | )
314 |
315 | func main() {
316 | mailjetClient := mailjet.NewMailjetClient(os.Getenv("MJ_APIKEY_PUBLIC"), os.Getenv("MJ_APIKEY_PRIVATE"))
317 | var data []resources.Contact
318 | _, _, err := mailjetClient.List("contact", &data)
319 | if err != nil {
320 | fmt.Println(err)
321 | }
322 | fmt.Printf("Data array: %+v\n", data)
323 | }
324 | ```
325 |
326 | #### Use filtering
327 |
328 | ```go
329 | // Retrieve all contacts that are not in the campaign exclusion list:
330 | package main
331 |
332 | import (
333 | "fmt"
334 | "os"
335 |
336 | "github.com/mailjet/mailjet-apiv3-go/v4"
337 | "github.com/mailjet/mailjet-apiv3-go/v4/resources"
338 | )
339 |
340 | func main() {
341 | mailjetClient := mailjet.NewMailjetClient(os.Getenv("MJ_APIKEY_PUBLIC"), os.Getenv("MJ_APIKEY_PRIVATE"))
342 | var data []resources.Contact
343 | _, _, err := mailjetClient.List("contact", &data, mailjet.Filter("IsExcludedFromCampaigns", "false"))
344 | if err != nil {
345 | fmt.Println(err)
346 | }
347 | fmt.Printf("Data array: %+v\n", data)
348 | }
349 | ```
350 |
351 | #### Retrieve a single object
352 |
353 | ```go
354 | // Retrieve a specific contact ID:
355 | package main
356 |
357 | import (
358 | "fmt"
359 | "os"
360 |
361 | "github.com/mailjet/mailjet-apiv3-go/v4"
362 | "github.com/mailjet/mailjet-apiv3-go/v4/resources"
363 | )
364 |
365 | func main() {
366 | mailjetClient := mailjet.NewMailjetClient(os.Getenv("MJ_APIKEY_PUBLIC"), os.Getenv("MJ_APIKEY_PRIVATE"))
367 | var data []resources.Contact
368 | mr := &mailjet.Request{
369 | Resource: "contact",
370 | ID: 5234, // replace with your contact ID here
371 | }
372 | err := mailjetClient.Get(mr, &data)
373 | if err != nil {
374 | fmt.Println(err)
375 | }
376 | fmt.Printf("Data array: %+v\n", data)
377 | }
378 | ```
379 |
380 | ### PUT request
381 |
382 | A `PUT` request in the Mailjet API will work as a `PATCH` request - the update will affect only the specified properties. The other properties of an existing resource will neither be modified, nor deleted. It also means that all non-mandatory properties can be omitted from your payload.
383 |
384 | Here's an example of a `PUT` request:
385 |
386 | ```go
387 | // Update the contact properties for a contact:
388 | package main
389 |
390 | import (
391 | "fmt"
392 | "os"
393 |
394 | "github.com/mailjet/mailjet-apiv3-go/v4"
395 | "github.com/mailjet/mailjet-apiv3-go/v4/resources"
396 | )
397 |
398 | func main() {
399 | mailjetClient := mailjet.NewMailjetClient(os.Getenv("MJ_APIKEY_PUBLIC"), os.Getenv("MJ_APIKEY_PRIVATE"))
400 | mr := &mailjet.Request{
401 | Resource: "contactdata",
402 | ID: 325, // replace with your contact ID here
403 | //AltID: "user1@example.com", // alternatively you can use contact's email
404 | }
405 | fmr := &mailjet.FullRequest{
406 | Info: mr,
407 | Payload: &resources.Contactdata{
408 | Data: resources.KeyValueList{
409 | {
410 | "Name": "name",
411 | "Value": "John",
412 | },
413 | {
414 | "Name": "country",
415 | "Value": "Canada",
416 | },
417 | },
418 | },
419 | }
420 | err := mailjetClient.Put(fmr, nil)
421 | if err != nil {
422 | fmt.Println(err)
423 | }
424 | }
425 | ```
426 |
427 | ### DELETE request
428 |
429 | Upon a successful DELETE request the response will not include a response body, but only a 204 No Content response code.
430 |
431 | Here's an example of a DELETE request:
432 |
433 | ```go
434 | // Delete an email template:
435 | package main
436 |
437 | import (
438 | "fmt"
439 | "os"
440 |
441 | "github.com/mailjet/mailjet-apiv3-go/v4"
442 | )
443 |
444 | func main() {
445 | mailjetClient := mailjet.NewMailjetClient(os.Getenv("MJ_APIKEY_PUBLIC"), os.Getenv("MJ_APIKEY_PRIVATE"))
446 |
447 | mr := &mailjet.Request{
448 | Resource: "template",
449 | ID: 423, // replace with your template ID here
450 | }
451 |
452 | err := mailjetClient.Delete(mr)
453 | if err != nil {
454 | fmt.Println(err)
455 | }
456 | }
457 | ```
458 |
459 | ## Contribute
460 |
461 | Mailjet loves developers. You can be part of this project!
462 |
463 | This wrapper is a great introduction to the open source world, check out the code!
464 |
465 | Feel free to ask anything, and contribute:
466 |
467 | - Fork the project.
468 | - Create a new branch.
469 | - Implement your feature or bug fix.
470 | - Add documentation to it.
471 | - Commit, push, open a pull request and voilà.
472 |
473 | If you have suggestions on how to improve the guides, please submit an issue in our [Official API Documentation repo](https://github.com/mailjet/api-documentation).
474 |
--------------------------------------------------------------------------------
/core.go:
--------------------------------------------------------------------------------
1 | package mailjet
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "io"
8 | "io/ioutil"
9 | "log"
10 | "net/http"
11 | "reflect"
12 | "runtime"
13 | "strconv"
14 | "strings"
15 | )
16 |
17 | // DebugLevel defines the verbosity of the debug.
18 | var DebugLevel int
19 |
20 | // These are the different level of debug.
21 | const (
22 | LevelNone = iota // No debug.
23 | LevelDebug // Debug without body.
24 | LevelDebugFull // Debug with body.
25 | )
26 |
27 | // User-Agent is formated as "UserAgentBase/UserAgentVersion;runtime.Version()".
28 | const (
29 | UserAgentBase = "mailjet-api-v3-go"
30 | UserAgentVersion = "4.0.1"
31 | )
32 |
33 | const (
34 | apiBase = "https://api.mailjet.com/v3"
35 | apiPath = "REST"
36 | dataPath = "DATA"
37 | )
38 |
39 | // createRequest is the main core function.
40 | func createRequest(method string, url string,
41 | payload interface{}, onlyFields []string,
42 | options ...RequestOptions) (req *http.Request, err error) {
43 |
44 | body, err := convertPayload(payload, onlyFields)
45 | if err != nil {
46 | return req, fmt.Errorf("creating request: %s\n", err)
47 | }
48 | req, err = http.NewRequest(method, url, bytes.NewBuffer(body))
49 | if err != nil {
50 | return req, fmt.Errorf("creating request: %s\n", err)
51 | }
52 | for _, option := range options {
53 | option(req)
54 | }
55 | userAgent(req)
56 | req.Header.Add("Accept", "application/json")
57 | return req, err
58 | }
59 |
60 | // converPayload returns payload casted in []byte.
61 | // If the payload is a structure, it's encoded to JSON.
62 | func convertPayload(payload interface{}, onlyFields []string) (body []byte, err error) {
63 | if payload != nil {
64 | switch t := payload.(type) {
65 | case string:
66 | body = []byte(t)
67 | case []byte:
68 | body = t
69 | default:
70 | v := reflect.Indirect(reflect.ValueOf(payload))
71 | if v.Kind() == reflect.Ptr {
72 | return convertPayload(v.Interface(), onlyFields)
73 | } else if v.Kind() == reflect.Struct {
74 | body, err = json.Marshal(buildMap(v, onlyFields))
75 | if err != nil {
76 | return body, err
77 | }
78 | }
79 | }
80 | if DebugLevel == LevelDebugFull {
81 | log.Println("Body:", string(body))
82 | }
83 | }
84 | return body, err
85 | }
86 |
87 | // buildMap returns a map with fields specified in onlyFields (all fields if nil)
88 | // and without the read_only fields.
89 | func buildMap(v reflect.Value, onlyFields []string) map[string]interface{} {
90 | res := make(map[string]interface{})
91 | if onlyFields != nil {
92 | for _, onlyField := range onlyFields {
93 | fieldType, exist := v.Type().FieldByName(onlyField)
94 | if exist {
95 | addFieldToMap(true, fieldType, v.FieldByName(onlyField), res)
96 | }
97 | }
98 | } else {
99 | for i := 0; i < v.NumField(); i++ {
100 | addFieldToMap(false, v.Type().Field(i), v.Field(i), res)
101 | }
102 | }
103 | return res
104 | }
105 |
106 | func addFieldToMap(onlyField bool, fieldType reflect.StructField,
107 | fieldValue reflect.Value, res map[string]interface{}) {
108 | if fieldType.Tag.Get("mailjet") != "read_only" {
109 | name, second := parseTag(fieldType.Tag.Get("json"))
110 | if name == "" {
111 | name = fieldType.Name
112 | }
113 | if !onlyField && second == "omitempty" &&
114 | isEmptyValue(fieldValue) {
115 | return
116 | }
117 | res[name] = fieldValue.Interface()
118 | }
119 | }
120 |
121 | func parseTag(tag string) (string, string) {
122 | if idx := strings.Index(tag, ","); idx != -1 {
123 | return tag[:idx], tag[idx+1:]
124 | }
125 | return tag, ""
126 | }
127 |
128 | func isEmptyValue(v reflect.Value) bool {
129 | switch v.Kind() {
130 | case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
131 | return v.Len() == 0
132 | case reflect.Bool:
133 | return !v.Bool()
134 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
135 | return v.Int() == 0
136 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
137 | return v.Uint() == 0
138 | case reflect.Float32, reflect.Float64:
139 | return v.Float() == 0
140 | case reflect.Interface, reflect.Ptr:
141 | return v.IsNil()
142 | }
143 | return false
144 | }
145 |
146 | // userAgent add the User-Agent value to the request header.
147 | func userAgent(req *http.Request) {
148 | ua := fmt.Sprintf("%s/%s;%s",
149 | UserAgentBase,
150 | UserAgentVersion,
151 | runtime.Version(),
152 | )
153 | req.Header.Add("User-Agent", ua)
154 | }
155 |
156 | func buildURL(baseURL string, info *Request) string {
157 | tokens := []string{baseURL, apiPath, info.Resource}
158 | if info.ID != 0 {
159 | id := strconv.FormatInt(info.ID, 10)
160 | tokens = append(tokens, id)
161 | } else if info.AltID != "" {
162 | tokens = append(tokens, string(info.AltID))
163 | }
164 | if info.Action != "" {
165 | tokens = append(tokens, info.Action)
166 | }
167 | if info.ActionID != 0 {
168 | actionID := strconv.FormatInt(info.ActionID, 10)
169 | tokens = append(tokens, actionID)
170 | }
171 | return strings.Join(tokens, "/")
172 | }
173 |
174 | func buildDataURL(baseURL string, info *DataRequest) string {
175 | tokens := []string{baseURL, dataPath, info.SourceType}
176 | if info.SourceTypeID != 0 {
177 | id := strconv.FormatInt(info.SourceTypeID, 10)
178 | tokens = append(tokens, id)
179 | }
180 | if info.DataType != "" {
181 | tokens = append(tokens, info.DataType)
182 | if info.MimeType != "" {
183 | tokens = append(tokens, info.MimeType)
184 | }
185 | }
186 | if info.DataTypeID != 0 {
187 | DataTypeID := strconv.FormatInt(info.DataTypeID, 10)
188 | tokens = append(tokens, DataTypeID)
189 | } else if info.LastID {
190 | tokens = append(tokens, "LAST")
191 | }
192 | return strings.Join(tokens, "/")
193 | }
194 |
195 | // readJsonResult decodes the API response, returns Count and Total values
196 | // and stores the Data in the value pointed to by data.
197 | func readJSONResult(r io.Reader, data interface{}) (int, int, error) {
198 | var res RequestResult
199 | res.Data = data
200 |
201 | jsonBlob, err := ioutil.ReadAll(r) // ReadAll and store in jsonBlob (mandatory if we want to unmarshal two times)
202 | if err != nil {
203 | return 0, 0, fmt.Errorf("Error reading API response: %s", err)
204 | }
205 | if DebugLevel == LevelDebugFull {
206 | log.Println("Body: ", string(jsonBlob)) // DEBUG
207 | }
208 |
209 | err = json.Unmarshal(jsonBlob, &res) // First try with the RequestResult struct
210 | if err != nil {
211 | return 0, 0, fmt.Errorf("Error decoding API response: %s", err)
212 | } else if _, ok := data.(**SentResult); ok { // Send API case
213 | err = json.Unmarshal(jsonBlob, data) // Trying directly with struct specified in parameter
214 | if err != nil {
215 | return 0, 0, fmt.Errorf("Error decoding API response: %s", err)
216 | }
217 | return 0, 0, nil // Count and Total are undetermined
218 | }
219 | return res.Count, res.Total, nil
220 | }
221 |
222 | // NbAttempt defines the number of attempt
223 | // for a request as long as StatusCode == 500.
224 | var NbAttempt = 5
225 |
226 | // doRequest is called to execute the request. Authentification is set
227 | // with the public key and the secret key specified in MailjetClient.
228 | func (c *HTTPClient) doRequest(req *http.Request) (resp *http.Response, err error) {
229 | if req == nil {
230 | return nil, fmt.Errorf("req is nil")
231 | }
232 |
233 | debugRequest(req) // DEBUG
234 | req.SetBasicAuth(c.apiKeyPublic, c.apiKeyPrivate)
235 | for attempt := 0; attempt < NbAttempt; attempt++ {
236 | if resp != nil {
237 | resp.Body.Close()
238 | }
239 | resp, err = c.client.Do(req)
240 | if err != nil || (resp != nil && resp.StatusCode != 500) {
241 | break
242 | }
243 | }
244 | defer debugResponse(resp) // DEBUG
245 | if err != nil {
246 | return resp, fmt.Errorf("Error getting %s: %s", req.URL, err)
247 | }
248 | err = checkResponseError(resp)
249 | return resp, err
250 | }
251 |
252 | // checkResponseError returns response error if the statuscode is < 200 or >= 400.
253 | func checkResponseError(resp *http.Response) error {
254 | if resp == nil {
255 | return fmt.Errorf("resp is nil")
256 | }
257 |
258 | if resp.StatusCode < 200 || resp.StatusCode >= 400 {
259 | var mailjetErr RequestError
260 | mailjetErr.StatusCode = resp.StatusCode
261 |
262 | b, err := ioutil.ReadAll(resp.Body)
263 | if err != nil {
264 | mailjetErr.ErrorMessage = "unable to read response body"
265 | mailjetErr.ErrorInfo = err.Error()
266 | return mailjetErr
267 | }
268 |
269 | err = json.Unmarshal(b, &mailjetErr)
270 | if err != nil {
271 | mailjetErr.ErrorMessage = "unexpected server response: %s" + string(b)
272 | mailjetErr.ErrorInfo = "json unmarshal error: " + err.Error()
273 | return mailjetErr
274 | }
275 | return mailjetErr
276 | }
277 |
278 | return nil
279 | }
280 |
281 | // debugRequest is a custom dump of the request.
282 | // Method used, final URl called, and Header content are logged.
283 | func debugRequest(req *http.Request) {
284 | if DebugLevel > LevelNone && req != nil {
285 | log.Printf("Method used is: %s\n", req.Method)
286 | log.Printf("Final URL is: %s\n", req.URL)
287 | log.Printf("Header is: %s\n", req.Header)
288 | }
289 | }
290 |
291 | // debugResponse is a custom dump of the response.
292 | // Status and Header content are logged.
293 | func debugResponse(resp *http.Response) {
294 | if DebugLevel > LevelNone && resp != nil {
295 | log.Printf("Status is: %s\n", resp.Status)
296 | log.Printf("Header is: %s\n", resp.Header)
297 | }
298 | }
299 |
--------------------------------------------------------------------------------
/core_test.go:
--------------------------------------------------------------------------------
1 | package mailjet
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "errors"
7 | "fmt"
8 | "io"
9 | "net/http"
10 | "runtime"
11 | "strings"
12 | "testing"
13 | )
14 |
15 | func TestCreateRequest(t *testing.T) {
16 | req, err := createRequest("GET", apiBase, nil, nil)
17 | if err != nil {
18 | t.Fatal("Unexpected error:", err)
19 | }
20 | if req.Method != "GET" {
21 | t.Fatal("Wrong method:", req.Method)
22 | }
23 | if req.URL.String() != apiBase {
24 | t.Fatal("Wrong URL:", req.URL.String())
25 | }
26 | ua := fmt.Sprintf("%s/%s;%s",
27 | UserAgentBase,
28 | UserAgentVersion,
29 | runtime.Version(),
30 | )
31 | if req.Header["User-Agent"] == nil || req.Header["User-Agent"][0] != ua {
32 | t.Fatal("Wrong User-agent:", req.Header["User-Agent"][0])
33 | }
34 | }
35 |
36 | func TestConvertPayload(t *testing.T) {
37 | type Test struct {
38 | ID int64 `mailjet:"read_only"`
39 | Name string
40 | Email string
41 | Address string `json:",omitempty" mailjet:"read_only"`
42 | TextPart string `json:"Text-Part,omitempty"`
43 | Header map[string]string `json:",omitempty"`
44 | MjCampaignID int64 `json:"Mj-CampaignID,omitempty" mailjet:"read_only"`
45 | }
46 | test := &Test{
47 | ID: -42,
48 | Email: "ex@mple.com",
49 | TextPart: "This is text",
50 | }
51 | resMap := make(map[string]interface{})
52 | resMap["Email"] = "ex@mple.com"
53 | resMap["Text-Part"] = "This is text"
54 | body, err := convertPayload(test, []string{"Email", "TextPart"})
55 | if err != nil {
56 | t.Fatal("Unexpected error:", err)
57 | }
58 | res, _ := json.Marshal(resMap)
59 | if !bytes.Equal(body, res) {
60 | t.Fatal("Wrong body:", string(body), string(res))
61 | }
62 |
63 | resMap["Name"] = ""
64 | body, err = convertPayload(&test, nil)
65 | if err != nil {
66 | t.Fatal("Unexpected error:", err)
67 | }
68 | res, _ = json.Marshal(resMap)
69 | if !bytes.Equal(body, res) {
70 | t.Fatal("Wrong body:", string(body), string(res))
71 | }
72 | }
73 |
74 | func TestBuildUrl(t *testing.T) {
75 | info := &Request{
76 | Resource: "contactslist",
77 | ID: 1,
78 | Action: "managemanycontacts",
79 | ActionID: 5,
80 | }
81 | expected := "https://api.mailjet.com/v3/REST/contactslist/1/managemanycontacts/5"
82 | res := buildURL(apiBase, info)
83 | if res != expected {
84 | t.Fatal("Fail to build URL:", res)
85 | }
86 | }
87 |
88 | func TestReadJsonEmptyResult(t *testing.T) {
89 | type TestStruct struct {
90 | Email string
91 | }
92 | var data []TestStruct
93 | body := `{"Count":0,"Data":[],"Total":0}`
94 | _, _, err := readJSONResult(strings.NewReader(body), &data)
95 | if err != nil {
96 | t.Fatal("Unexpected error:", err)
97 | }
98 | }
99 |
100 | func TestReadJsonResult(t *testing.T) {
101 | type TestStruct struct {
102 | Email string
103 | }
104 | var data []TestStruct
105 | body := `{"Count":2,"Data":[{"Email":"qwe@qwe.com"},{"Email":"aze@aze.com"}],"Total":1}`
106 | count, total, err := readJSONResult(strings.NewReader(body), &data)
107 | if err != nil {
108 | t.Fatal("Unexpected error:", err)
109 | }
110 | if count != 2 {
111 | t.Fatalf("Wrong count: %d != %d", count, 2)
112 | }
113 | if total != 1 {
114 | t.Fatalf("Wrong total: %d != %d", total, 2)
115 | }
116 | if data != nil {
117 | if data[0].Email != "qwe@qwe.com" {
118 | t.Fatalf("Fail to unmarshal JSON: %s != %s", data[0].Email, "qwe@qwe.com")
119 | }
120 | if data[1].Email != "aze@aze.com" {
121 | t.Fatalf("Fail to unmarshal JSON: %s != %s", data[1].Email, "aze@aze.com")
122 | }
123 | } else {
124 | t.Fatal("Fail to unmarshal JSON: empty res")
125 | }
126 | }
127 |
128 | func Test_checkResponseError(t *testing.T) {
129 | t.Run("4xx", func(t *testing.T) {
130 | const statusCode = 404
131 |
132 | resp := &http.Response{
133 | StatusCode: statusCode,
134 | Body: io.NopCloser(strings.NewReader(`{"ErrorMessage":"foo not found"}`)),
135 | }
136 |
137 | err := checkResponseError(resp)
138 | if err == nil {
139 | t.Fatal("Expected error")
140 | }
141 |
142 | var mailjetErr RequestError
143 | if errors.As(err, &mailjetErr) {
144 | if mailjetErr.StatusCode != statusCode {
145 | t.Fatalf("Status code: exptected(%d) but got(%d)", statusCode, mailjetErr.StatusCode)
146 | }
147 | } else {
148 | t.Fatalf("err(%v) must be a RequestError type", err)
149 | }
150 | })
151 | }
152 |
--------------------------------------------------------------------------------
/data_api.go:
--------------------------------------------------------------------------------
1 | package mailjet
2 |
3 | import "strings"
4 |
5 | // ListData issues a GET to list the specified data resource
6 | // and stores the result in the value pointed to by res.
7 | // Filters can be add via functional options.
8 | func (c *Client) ListData(resource string, resp interface{}, options ...RequestOptions) (count, total int, err error) {
9 | url := buildDataURL(c.apiBase, &DataRequest{SourceType: resource})
10 | req, err := createRequest("GET", url, nil, nil, options...)
11 | if err != nil {
12 | return count, total, err
13 | }
14 |
15 | return c.httpClient.Send(req).Read(resp).Call()
16 | }
17 |
18 | // GetData issues a GET to view a resource specifying an id
19 | // and stores the result in the value pointed to by res.
20 | // Filters can be add via functional options.
21 | // Without an specified SourceTypeID in MailjetDataRequest, it is the same as ListData.
22 | func (c *Client) GetData(mdr *DataRequest, res interface{}, options ...RequestOptions) (err error) {
23 | url := buildDataURL(c.apiBase, mdr)
24 | req, err := createRequest("GET", url, nil, nil, options...)
25 | if err != nil {
26 | return err
27 | }
28 |
29 | _, _, err = c.httpClient.Send(req).Read(res).Call()
30 | return err
31 | }
32 |
33 | // PostData issues a POST to create a new data resource
34 | // and stores the result in the value pointed to by res.
35 | // Filters can be add via functional options.
36 | func (c *Client) PostData(fmdr *FullDataRequest, res interface{}, options ...RequestOptions) (err error) {
37 | url := buildDataURL(c.apiBase, fmdr.Info)
38 | req, err := createRequest("POST", url, fmdr.Payload, nil, options...)
39 | if err != nil {
40 | return err
41 | }
42 |
43 | headers := map[string]string{"Content-Type": "application/json"}
44 | if fmdr.Info.MimeType != "" {
45 | contentType := strings.Replace(fmdr.Info.MimeType, ":", "/", 1)
46 | headers = map[string]string{"Content-Type": contentType}
47 | }
48 |
49 | _, _, err = c.httpClient.Send(req).With(headers).Read(res).Call()
50 | return err
51 | }
52 |
53 | // PutData is used to update a data resource.
54 | // Fields to be updated must be specified by the string array onlyFields.
55 | // If onlyFields is nil, all fields except these with the tag read_only, are updated.
56 | // Filters can be add via functional options.
57 | func (c *Client) PutData(fmr *FullDataRequest, onlyFields []string, options ...RequestOptions) (err error) {
58 | url := buildDataURL(c.apiBase, fmr.Info)
59 | req, err := createRequest("PUT", url, fmr.Payload, onlyFields, options...)
60 | if err != nil {
61 | return err
62 | }
63 |
64 | headers := map[string]string{"Content-Type": "application/json"}
65 | _, _, err = c.httpClient.Send(req).With(headers).Call()
66 |
67 | return err
68 | }
69 |
70 | // DeleteData is used to delete a data resource.
71 | func (c *Client) DeleteData(mdr *DataRequest, options ...RequestOptions) (err error) {
72 | url := buildDataURL(c.apiBase, mdr)
73 | req, err := createRequest("DELETE", url, nil, nil, options...)
74 | if err != nil {
75 | return err
76 | }
77 |
78 | _, _, err = c.httpClient.Send(req).Call()
79 |
80 | return err
81 | }
82 |
--------------------------------------------------------------------------------
/example_test.go:
--------------------------------------------------------------------------------
1 | package mailjet_test
2 |
3 | import (
4 | "fmt"
5 | "net/textproto"
6 | "os"
7 |
8 | "github.com/mailjet/mailjet-apiv3-go/v4"
9 | "github.com/mailjet/mailjet-apiv3-go/v4/resources"
10 | )
11 |
12 | var (
13 | publicKey = os.Getenv("MJ_APIKEY_PUBLIC")
14 | privateKey = os.Getenv("MJ_APIKEY_PRIVATE")
15 | )
16 |
17 | func exampleMailjetClientList() {
18 | mj := mailjet.NewMailjetClient(publicKey, privateKey)
19 |
20 | var res []resources.Metadata
21 | count, total, err := mj.List("metadata", &res)
22 | if err != nil {
23 | fmt.Fprintln(os.Stderr, err)
24 | os.Exit(1)
25 | }
26 | fmt.Printf("Count: %d\nTotal: %d\n", count, total)
27 |
28 | fmt.Println("Resources:")
29 | for _, resource := range res {
30 | fmt.Println(resource.Name)
31 | }
32 | }
33 |
34 | func exampleMailjetClientGet() {
35 | publicKey := os.Getenv("MJ_APIKEY_PUBLIC")
36 | secretKey := os.Getenv("MJ_APIKEY_PRIVATE")
37 |
38 | mj := mailjet.NewMailjetClient(publicKey, secretKey)
39 |
40 | var senders []resources.Sender
41 | info := &mailjet.Request{
42 | Resource: "sender",
43 | AltID: "qwe@qwe.com",
44 | }
45 | err := mj.Get(info, &senders)
46 | if err != nil {
47 | fmt.Fprintln(os.Stderr, err)
48 | os.Exit(1)
49 | }
50 | if senders != nil {
51 | fmt.Printf("Sender struct: %+v\n", senders[0])
52 | }
53 | }
54 |
55 | func exampleMailjetClientPost() {
56 | publicKey := os.Getenv("MJ_APIKEY_PUBLIC")
57 | secretKey := os.Getenv("MJ_APIKEY_PRIVATE")
58 |
59 | mj := mailjet.NewMailjetClient(publicKey, secretKey)
60 |
61 | var senders []resources.Sender
62 | fmr := &mailjet.FullRequest{
63 | Info: &mailjet.Request{Resource: "sender"},
64 | Payload: &resources.Sender{Name: "Default", Email: "qwe@qwe.com"},
65 | }
66 | err := mj.Post(fmr, &senders)
67 | if err != nil {
68 | fmt.Fprintln(os.Stderr, err)
69 | os.Exit(1)
70 | }
71 | if senders != nil {
72 | fmt.Printf("Data struct: %+v\n", senders[0])
73 | }
74 | }
75 |
76 | func exampleMailjetClientPut() {
77 | publicKey := os.Getenv("MJ_APIKEY_PUBLIC")
78 | secretKey := os.Getenv("MJ_APIKEY_PRIVATE")
79 |
80 | mj := mailjet.NewMailjetClient(publicKey, secretKey)
81 |
82 | fmr := &mailjet.FullRequest{
83 | Info: &mailjet.Request{Resource: "sender", AltID: "qwe@qwe.com"},
84 | Payload: &resources.Sender{Name: "Bob", IsDefaultSender: true},
85 | }
86 | err := mj.Put(fmr, []string{"Name", "IsDefaultSender"})
87 | if err != nil {
88 | fmt.Fprintln(os.Stderr, err)
89 | os.Exit(1)
90 | } else {
91 | fmt.Println("Success")
92 | }
93 | }
94 |
95 | func exampleMailjetClientDelete() {
96 | publicKey := os.Getenv("MJ_APIKEY_PUBLIC")
97 | secretKey := os.Getenv("MJ_APIKEY_PRIVATE")
98 |
99 | mj := mailjet.NewMailjetClient(publicKey, secretKey)
100 |
101 | info := &mailjet.Request{
102 | Resource: "sender",
103 | AltID: "qwe@qwe.com",
104 | }
105 | err := mj.Delete(info)
106 | if err != nil {
107 | fmt.Fprintln(os.Stderr, err)
108 | os.Exit(1)
109 | } else {
110 | fmt.Println("Success")
111 | }
112 | }
113 |
114 | func exampleMailjetClientSendMail() {
115 | publicKey := os.Getenv("MJ_APIKEY_PUBLIC")
116 | secretKey := os.Getenv("MJ_APIKEY_PRIVATE")
117 |
118 | mj := mailjet.NewMailjetClient(publicKey, secretKey)
119 |
120 | param := &mailjet.InfoSendMail{
121 | FromEmail: "qwe@qwe.com",
122 | FromName: "Bob Patrick",
123 | Recipients: []mailjet.Recipient{
124 | {
125 | Email: "qwe@qwe.com",
126 | },
127 | },
128 | Subject: "Hello World!",
129 | TextPart: "Hi there !",
130 | }
131 | res, err := mj.SendMail(param)
132 | if err != nil {
133 | fmt.Println(err)
134 | } else {
135 | fmt.Println("Success")
136 | fmt.Println(res)
137 | }
138 | }
139 |
140 | func exampleMailjetClientSendMailSMTP() {
141 | mj := mailjet.NewMailjetClient(publicKey, privateKey)
142 |
143 | header := make(textproto.MIMEHeader)
144 | header.Add("From", "qwe@qwe.com")
145 | header.Add("To", "qwe@qwe.com")
146 | header.Add("Subject", "Hello World!")
147 | header.Add("X-Mailjet-Campaign", "test")
148 | content := []byte("Hi there !")
149 | info := &mailjet.InfoSMTP{
150 | From: "qwe@qwe.com",
151 | Recipients: header["To"],
152 | Header: header,
153 | Content: content,
154 | }
155 | err := mj.SendMailSMTP(info)
156 | if err != nil {
157 | fmt.Println(err)
158 | } else {
159 | fmt.Println("Success")
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/fixtures/fixtures.go:
--------------------------------------------------------------------------------
1 | // Package fixtures provid fake data so we can mock the `Client` struct in mailjet_client.go
2 | package fixtures
3 |
4 | import (
5 | "encoding/json"
6 | "errors"
7 | "reflect"
8 |
9 | "github.com/mailjet/mailjet-apiv3-go/v4/resources"
10 | )
11 |
12 | // Fixtures definition
13 | type Fixtures struct {
14 | data map[interface{}][]byte
15 | }
16 |
17 | // New loads fixtures in memory by iterating through its fixture method
18 | func New() *Fixtures {
19 | f := new(Fixtures)
20 | f.data = make(map[interface{}][]byte)
21 | fix := reflect.ValueOf(f)
22 | for i := 0; i < fix.NumMethod(); i++ {
23 | method := fix.Method(i)
24 | if method.Type().NumIn() == 0 && method.Type().NumOut() == 2 {
25 | values := method.Call([]reflect.Value{})
26 | reflect.ValueOf(f.data).SetMapIndex(values[0], values[1])
27 | }
28 | }
29 |
30 | return f
31 | }
32 |
33 | func (f *Fixtures) Read(v interface{}) error {
34 | for t, val := range f.data {
35 | if reflect.ValueOf(v).Type().String() == reflect.ValueOf(t).Type().String() {
36 | return json.Unmarshal(val, v)
37 | }
38 | }
39 | return errors.New("not found")
40 | }
41 |
42 | // User fixture info
43 | func (f *Fixtures) User() (*[]resources.User, []byte) {
44 | return &[]resources.User{}, []byte(`[{"ID":24, "Email": "passenger@mailjet.com", "LastIP": "127.0.0.1", "Username": "passenger", "MaxAllowedAPIKeys": 5}]`)
45 | }
46 |
47 | // Contact fixture info
48 | func (f *Fixtures) Contact() (*[]resources.Contact, []byte) {
49 | return &[]resources.Contact{}, []byte(`[{"ID":42, "Email": "contact@mailjet.com", "DeliveredCount": 42}]`)
50 | }
51 |
52 | // ContactList fixture info
53 | func (f *Fixtures) ContactList() (*[]resources.Contactslist, []byte) {
54 | return &[]resources.Contactslist{}, []byte(`[{"ID":84, "Address": "contact@mailjet.com", "Name": "John Doe", "SubscriberCount": 1000}]`)
55 | }
56 |
57 | // ListRecipient fixture info
58 | func (f *Fixtures) ListRecipient() (*[]resources.Listrecipient, []byte) {
59 | return &[]resources.Listrecipient{}, []byte(`[{"ID":168}]`)
60 | }
61 |
62 | // Sender fixture info
63 | func (f *Fixtures) Sender() (*[]resources.Sender, []byte) {
64 | return &[]resources.Sender{}, []byte(`[{"ID":336, "Name": "Mansa Musa", "Status": "Active", "EmailType": "transactional"}]`)
65 | }
66 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/mailjet/mailjet-apiv3-go/v4
2 |
3 | go 1.13
4 |
--------------------------------------------------------------------------------
/http_client.go:
--------------------------------------------------------------------------------
1 | package mailjet
2 |
3 | import (
4 | "encoding/csv"
5 | "fmt"
6 | "net/http"
7 | "strings"
8 | "sync"
9 | )
10 |
11 | // HTTPClient is a wrapper around http.Client
12 | type HTTPClient struct {
13 | client *http.Client
14 | apiKeyPublic string
15 | apiKeyPrivate string
16 | headers map[string]string
17 | request *http.Request
18 | response interface{}
19 | mu sync.RWMutex
20 | }
21 |
22 | // NewHTTPClient returns a new httpClient
23 | func NewHTTPClient(apiKeyPublic, apiKeyPrivate string) *HTTPClient {
24 | return &HTTPClient{
25 | apiKeyPublic: apiKeyPublic,
26 | apiKeyPrivate: apiKeyPrivate,
27 | client: &http.Client{},
28 | }
29 | }
30 |
31 | // APIKeyPublic returns the public key.
32 | func (c *HTTPClient) APIKeyPublic() string {
33 | return c.apiKeyPublic
34 | }
35 |
36 | // APIKeyPrivate returns the secret key.
37 | func (c *HTTPClient) APIKeyPrivate() string {
38 | return c.apiKeyPrivate
39 | }
40 |
41 | // Client returns the underlying http client
42 | func (c *HTTPClient) Client() *http.Client {
43 | c.mu.RLock()
44 | defer c.mu.RUnlock()
45 |
46 | return c.client
47 | }
48 |
49 | // SetClient sets the underlying http client
50 | func (c *HTTPClient) SetClient(client *http.Client) {
51 | c.mu.Lock()
52 | defer c.mu.Unlock()
53 |
54 | c.client = client
55 | }
56 |
57 | // Send binds the request to the underlying http client
58 | func (c *HTTPClient) Send(req *http.Request) HTTPClientInterface {
59 | c.mu.Lock()
60 | defer c.mu.Unlock()
61 |
62 | c.request = req
63 | return c
64 | }
65 |
66 | // With binds the header to the underlying http client
67 | func (c *HTTPClient) With(headers map[string]string) HTTPClientInterface {
68 | c.mu.Lock()
69 | defer c.mu.Unlock()
70 |
71 | c.headers = headers
72 | return c
73 | }
74 |
75 | // SendMailV31 simply calls the underlying http client.Do function
76 | func (c *HTTPClient) SendMailV31(req *http.Request) (*http.Response, error) {
77 | res, err := c.Client().Do(req)
78 | return res, err
79 | }
80 |
81 | // Read binds the response to the underlying http client
82 | func (c *HTTPClient) Read(response interface{}) HTTPClientInterface {
83 | c.mu.Lock()
84 | defer c.mu.Unlock()
85 |
86 | c.response = response
87 | return c
88 | }
89 |
90 | // Call execute the HTTP call to the API
91 | func (c *HTTPClient) Call() (count, total int, err error) {
92 | c.mu.RLock()
93 | defer c.mu.RUnlock()
94 |
95 | if c.request == nil {
96 | return 0, 0, fmt.Errorf("request is nil")
97 | }
98 |
99 | defer c.reset()
100 | for key, value := range c.headers {
101 | c.request.Header.Add(key, value)
102 | }
103 |
104 | resp, err := c.doRequest(c.request)
105 | if resp != nil {
106 | defer resp.Body.Close()
107 | }
108 |
109 | if err != nil {
110 | return count, total, err
111 | } else if resp == nil {
112 | return count, total, fmt.Errorf("empty response")
113 | }
114 |
115 | if c.response != nil {
116 | if resp.Header["Content-Type"] != nil {
117 | contentType := strings.ToLower(resp.Header["Content-Type"][0])
118 | if strings.Contains(contentType, "application/json") {
119 | return readJSONResult(resp.Body, c.response)
120 | } else if strings.Contains(contentType, "text/csv") {
121 | c.response, err = csv.NewReader(resp.Body).ReadAll()
122 | }
123 | }
124 | }
125 |
126 | return count, total, err
127 | }
128 |
129 | func (c *HTTPClient) reset() {
130 | c.headers = make(map[string]string)
131 | c.request = nil
132 | c.response = nil
133 | }
134 |
--------------------------------------------------------------------------------
/http_client_decl.go:
--------------------------------------------------------------------------------
1 | package mailjet
2 |
3 | import "net/http"
4 |
5 | // HTTPClientInterface method definition
6 | type HTTPClientInterface interface {
7 | APIKeyPublic() string
8 | APIKeyPrivate() string
9 | Client() *http.Client
10 | SetClient(client *http.Client)
11 | Send(req *http.Request) HTTPClientInterface
12 | SendMailV31(req *http.Request) (*http.Response, error)
13 | With(headers map[string]string) HTTPClientInterface
14 | Read(response interface{}) HTTPClientInterface
15 | Call() (count int, total int, err error)
16 | }
17 |
--------------------------------------------------------------------------------
/http_client_mock.go:
--------------------------------------------------------------------------------
1 | package mailjet
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "log"
7 | "net/http"
8 |
9 | "github.com/mailjet/mailjet-apiv3-go/v4/fixtures"
10 | )
11 |
12 | // HTTPClientMock definition
13 | type HTTPClientMock struct {
14 | client *http.Client
15 | apiKeyPublic string
16 | apiKeyPrivate string
17 | headers map[string]string
18 | request *http.Request
19 | response interface{}
20 | validCreds bool
21 | fx *fixtures.Fixtures
22 | CallFunc func() (int, int, error)
23 | SendMailV31Func func(req *http.Request) (*http.Response, error)
24 | }
25 |
26 | // NewhttpClientMock instanciate new httpClientMock
27 | func NewhttpClientMock(valid bool) *HTTPClientMock {
28 | return &HTTPClientMock{
29 | apiKeyPublic: "apiKeyPublic",
30 | apiKeyPrivate: "apiKeyPrivate",
31 | client: http.DefaultClient,
32 | validCreds: valid,
33 | fx: fixtures.New(),
34 | CallFunc: func() (int, int, error) {
35 | if valid {
36 | return 1, 1, nil
37 | }
38 | return 0, 0, errors.New("Unexpected error: Unexpected server response code: 401: EOF")
39 | },
40 | SendMailV31Func: func(req *http.Request) (*http.Response, error) {
41 | return nil, errors.New("mock send mail function not implemented yet")
42 | },
43 | }
44 | }
45 |
46 | // APIKeyPublic returns the public key.
47 | func (c *HTTPClientMock) APIKeyPublic() string {
48 | return c.apiKeyPublic
49 | }
50 |
51 | // APIKeyPrivate returns the secret key.
52 | func (c *HTTPClientMock) APIKeyPrivate() string {
53 | return c.apiKeyPrivate
54 | }
55 |
56 | // Client returns the underlying http client
57 | func (c *HTTPClientMock) Client() *http.Client {
58 | return c.client
59 | }
60 |
61 | // SetClient allow to set the underlying http client
62 | func (c *HTTPClientMock) SetClient(client *http.Client) {
63 | c.client = client
64 | }
65 |
66 | // Send data through HTTP with the current configuration
67 | func (c *HTTPClientMock) Send(req *http.Request) HTTPClientInterface {
68 | c.request = req
69 | return c
70 | }
71 |
72 | // With lets you set the http header and returns the httpClientMock with the header modified
73 | func (c *HTTPClientMock) With(headers map[string]string) HTTPClientInterface {
74 | c.headers = headers
75 | return c
76 | }
77 |
78 | // Read allow you to bind the response received through the underlying http client
79 | func (c *HTTPClientMock) Read(response interface{}) HTTPClientInterface {
80 | err := c.fx.Read(response)
81 | if err != nil {
82 | log.Println(fmt.Errorf("c.fx.Read: %w", err))
83 | }
84 |
85 | return c
86 | }
87 |
88 | // SendMailV31 mock function
89 | func (c *HTTPClientMock) SendMailV31(req *http.Request) (*http.Response, error) {
90 | return c.SendMailV31Func(req)
91 | }
92 |
93 | // Call the mailjet API
94 | func (c *HTTPClientMock) Call() (int, int, error) {
95 | return c.CallFunc()
96 | }
97 |
--------------------------------------------------------------------------------
/mailjet_client.go:
--------------------------------------------------------------------------------
1 | // Package mailjet provides methods for interacting with the last version of the Mailjet API.
2 | // The goal of this component is to simplify the usage of the MailJet API for GO developers.
3 | //
4 | // For more details, see the full API Documentation at http://dev.mailjet.com/
5 | package mailjet
6 |
7 | import (
8 | "bytes"
9 | "context"
10 | "encoding/json"
11 | "fmt"
12 | "io"
13 | "log"
14 | "net/http"
15 | "net/textproto"
16 | "os"
17 | "strings"
18 | )
19 |
20 | // NewMailjetClient returns a new MailjetClient using an public apikey
21 | // and an secret apikey to be used when authenticating to API.
22 | func NewMailjetClient(apiKeyPublic, apiKeyPrivate string, baseURL ...string) *Client {
23 | httpClient := NewHTTPClient(apiKeyPublic, apiKeyPrivate)
24 | smtpClient := NewSMTPClient(apiKeyPublic, apiKeyPrivate)
25 |
26 | client := &Client{
27 | httpClient: httpClient,
28 | smtpClient: smtpClient,
29 | apiBase: apiBase,
30 | }
31 |
32 | if len(baseURL) > 0 {
33 | client.apiBase = baseURL[0]
34 | }
35 | return client
36 | }
37 |
38 | // NewClient function
39 | func NewClient(httpCl HTTPClientInterface, smtpCl SMTPClientInterface, baseURL ...string) *Client {
40 |
41 | client := &Client{
42 | httpClient: httpCl,
43 | smtpClient: smtpCl,
44 | apiBase: apiBase,
45 | }
46 |
47 | if len(baseURL) > 0 {
48 | client.apiBase = baseURL[0]
49 | }
50 | return client
51 | }
52 |
53 | // SetBaseURL sets the base URL
54 | func (c *Client) SetBaseURL(baseURL string) {
55 | c.apiBase = baseURL
56 | }
57 |
58 | // APIKeyPublic returns the public key.
59 | func (c *Client) APIKeyPublic() string {
60 | return c.httpClient.APIKeyPublic()
61 | }
62 |
63 | // APIKeyPrivate returns the secret key.
64 | func (c *Client) APIKeyPrivate() string {
65 | return c.httpClient.APIKeyPrivate()
66 |
67 | }
68 |
69 | // SetURL function to set the base url of the wrapper instance
70 | func (c *Client) SetURL(baseURL string) {
71 | c.Lock()
72 | defer c.Unlock()
73 | c.apiBase = baseURL
74 | }
75 |
76 | // Client returns the underlying http client
77 | func (c *Client) Client() *http.Client {
78 | return c.httpClient.Client()
79 | }
80 |
81 | // SetClient allows to customize http client.
82 | func (c *Client) SetClient(client *http.Client) {
83 | c.Lock()
84 | defer c.Unlock()
85 | c.httpClient.SetClient(client)
86 | }
87 |
88 | // Filter applies a filter with the defined key and value.
89 | func Filter(key, value string) RequestOptions {
90 | return func(req *http.Request) {
91 | q := req.URL.Query()
92 | q.Add(key, value)
93 | req.URL.RawQuery = strings.Replace(q.Encode(), "%2B", "+", 1)
94 | }
95 | }
96 |
97 | // WithContext sets the request context
98 | func WithContext(ctx context.Context) RequestOptions {
99 | return func(req *http.Request) {
100 | *req = *(req.WithContext(ctx))
101 | }
102 | }
103 |
104 | // SortOrder defines the order of the result.
105 | type SortOrder int
106 |
107 | // These are the two possible order.
108 | const (
109 | SortDesc = SortOrder(iota)
110 | SortAsc
111 | )
112 |
113 | var debugOut io.Writer = os.Stderr
114 |
115 | // SetDebugOutput sets the output destination for the debug.
116 | func SetDebugOutput(w io.Writer) {
117 | debugOut = w
118 | log.SetOutput(w)
119 | }
120 |
121 | // Sort applies the Sort filter to the request.
122 | func Sort(value string, order SortOrder) RequestOptions {
123 | if order == SortDesc {
124 | value += "+DESC"
125 | }
126 | return Filter("Sort", value)
127 | }
128 |
129 | // List issues a GET to list the specified resource
130 | // and stores the result in the value pointed to by res.
131 | // Filters can be add via functional options.
132 | func (c *Client) List(resource string, resp interface{}, options ...RequestOptions) (count, total int, err error) {
133 | url := buildURL(c.apiBase, &Request{Resource: resource})
134 | req, err := createRequest("GET", url, nil, nil, options...)
135 | if err != nil {
136 | return count, total, err
137 | }
138 |
139 | c.Lock()
140 | defer c.Unlock()
141 | return c.httpClient.Send(req).Read(resp).Call()
142 | }
143 |
144 | // Get issues a GET to view a resource specifying an id
145 | // and stores the result in the value pointed to by res.
146 | // Filters can be add via functional options.
147 | // Without an specified ID in MailjetRequest, it is the same as List.
148 | func (c *Client) Get(mr *Request, resp interface{}, options ...RequestOptions) (err error) {
149 | url := buildURL(c.apiBase, mr)
150 | req, err := createRequest("GET", url, nil, nil, options...)
151 | if err != nil {
152 | return err
153 | }
154 |
155 | c.Lock()
156 | defer c.Unlock()
157 | _, _, err = c.httpClient.Send(req).Read(resp).Call()
158 | return err
159 | }
160 |
161 | // Post issues a POST to create a new resource
162 | // and stores the result in the value pointed to by res.
163 | // Filters can be add via functional options.
164 | func (c *Client) Post(fmr *FullRequest, resp interface{}, options ...RequestOptions) (err error) {
165 | url := buildURL(c.apiBase, fmr.Info)
166 | req, err := createRequest("POST", url, fmr.Payload, nil, options...)
167 | if err != nil {
168 | return err
169 | }
170 |
171 | headers := map[string]string{"Content-Type": "application/json"}
172 | c.Lock()
173 | defer c.Unlock()
174 | _, _, err = c.httpClient.Send(req).With(headers).Read(resp).Call()
175 | return err
176 | }
177 |
178 | // Put is used to update a resource.
179 | // Fields to be updated must be specified by the string array onlyFields.
180 | // If onlyFields is nil, all fields except these with the tag read_only, are updated.
181 | // Filters can be add via functional options.
182 | func (c *Client) Put(fmr *FullRequest, onlyFields []string, options ...RequestOptions) (err error) {
183 | url := buildURL(c.apiBase, fmr.Info)
184 | req, err := createRequest("PUT", url, fmr.Payload, onlyFields, options...)
185 | if err != nil {
186 | return err
187 | }
188 |
189 | headers := map[string]string{"Content-Type": "application/json"}
190 | c.Lock()
191 | defer c.Unlock()
192 | _, _, err = c.httpClient.Send(req).With(headers).Call()
193 | return err
194 | }
195 |
196 | // Delete is used to delete a resource.
197 | func (c *Client) Delete(mr *Request) (err error) {
198 | url := buildURL(c.apiBase, mr)
199 | req, err := createRequest("DELETE", url, nil, nil)
200 | if err != nil {
201 | return err
202 | }
203 |
204 | c.Lock()
205 | defer c.Unlock()
206 | _, _, err = c.httpClient.Send(req).Call()
207 | return err
208 | }
209 |
210 | // SendMail send mail via API.
211 | func (c *Client) SendMail(data *InfoSendMail, options ...RequestOptions) (res *SentResult, err error) {
212 | url := c.apiBase + "/send/message"
213 | req, err := createRequest("POST", url, data, nil, options...)
214 | if err != nil {
215 | return res, err
216 | }
217 |
218 | headers := map[string]string{"Content-Type": "application/json"}
219 |
220 | c.Lock()
221 | defer c.Unlock()
222 | _, _, err = c.httpClient.Send(req).With(headers).Read(&res).Call()
223 | return res, err
224 | }
225 |
226 | // SendMailSMTP send mail via SMTP.
227 | func (c *Client) SendMailSMTP(info *InfoSMTP) error {
228 | return c.smtpClient.SendMail(
229 | info.From,
230 | info.Recipients,
231 | buildMessage(info.Header, info.Content))
232 | }
233 |
234 | func buildMessage(header textproto.MIMEHeader, content []byte) []byte {
235 | buff := bytes.NewBuffer(nil)
236 | for key, values := range header {
237 | buff.WriteString(fmt.Sprintf("%s: %s\r\n", key, strings.Join(values, ", ")))
238 | }
239 | buff.WriteString("\r\n")
240 | buff.Write(content)
241 |
242 | return buff.Bytes()
243 | }
244 |
245 | // SendMailV31 sends a mail to the send API v3.1
246 | func (c *Client) SendMailV31(data *MessagesV31, options ...RequestOptions) (*ResultsV31, error) {
247 | url := c.apiBase + ".1/send"
248 | req, err := createRequest("POST", url, data, nil, options...)
249 | if err != nil {
250 | return nil, err
251 | }
252 |
253 | req.Header.Set("Content-Type", "application/json")
254 | req.SetBasicAuth(c.APIKeyPublic(), c.APIKeyPrivate())
255 |
256 | r, err := c.httpClient.SendMailV31(req)
257 | if err != nil {
258 | return nil, err
259 | }
260 | defer r.Body.Close()
261 |
262 | decoder := json.NewDecoder(r.Body)
263 |
264 | switch r.StatusCode {
265 | case http.StatusOK:
266 |
267 | var res ResultsV31
268 | if err := decoder.Decode(&res); err != nil {
269 | return nil, err
270 | }
271 | return &res, nil
272 |
273 | case http.StatusBadRequest, http.StatusForbidden:
274 |
275 | var apiFeedbackErr APIFeedbackErrorsV31
276 | if err := decoder.Decode(&apiFeedbackErr); err != nil {
277 | return nil, err
278 | }
279 | return nil, &apiFeedbackErr
280 |
281 | default:
282 |
283 | var errInfo ErrorInfoV31
284 | if err := decoder.Decode(&errInfo); err != nil {
285 | return nil, err
286 | }
287 | return nil, &errInfo
288 | }
289 | }
290 |
--------------------------------------------------------------------------------
/mailjet_client_decl.go:
--------------------------------------------------------------------------------
1 | // Package mailjet provides methods for interacting with the last version of the Mailjet API.
2 | // The goal of this component is to simplify the usage of the MailJet API for GO developers.
3 | //
4 | // For more details, see the full API Documentation at http://dev.mailjet.com/
5 | package mailjet
6 |
7 | import "net/http"
8 |
9 | // ClientInterface defines all Client functions.
10 | type ClientInterface interface {
11 | APIKeyPublic() string
12 | APIKeyPrivate() string
13 | Client() *http.Client
14 | SetClient(client *http.Client)
15 | List(resource string, resp interface{}, options ...RequestOptions) (count, total int, err error)
16 | Get(mr *Request, resp interface{}, options ...RequestOptions) error
17 | Post(fmr *FullRequest, resp interface{}, options ...RequestOptions) error
18 | Put(fmr *FullRequest, onlyFields []string, options ...RequestOptions) error
19 | Delete(mr *Request, options ...RequestOptions) error
20 | SendMail(data *InfoSendMail) (*SentResult, error)
21 | SendMailSMTP(info *InfoSMTP) (err error)
22 | }
23 |
24 | // ClientInterfaceV31 defines the Client functions, including SendMailV31
25 | type ClientInterfaceV31 interface {
26 | ClientInterface
27 | SendMailV31(data *MessagesV31) (*ResultsV31, error)
28 | }
29 |
--------------------------------------------------------------------------------
/mailjet_client_test.go:
--------------------------------------------------------------------------------
1 | package mailjet_test
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "io/ioutil"
8 | "math/rand"
9 | "net/http"
10 | "net/http/httptest"
11 | "reflect"
12 | "testing"
13 | "time"
14 |
15 | "github.com/mailjet/mailjet-apiv3-go/v4"
16 | "github.com/mailjet/mailjet-apiv3-go/v4/resources"
17 | )
18 |
19 | var (
20 | letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
21 | // defaultMessages is the default message passed to the server when an email
22 | // is sent.
23 | defaultMessages = mailjet.MessagesV31{
24 | Info: []mailjet.InfoMessagesV31{
25 | {
26 | From: &mailjet.RecipientV31{
27 | Email: "passenger@mailjet.com",
28 | Name: "passenger",
29 | },
30 | To: &mailjet.RecipientsV31{
31 | mailjet.RecipientV31{
32 | Email: "recipient@company.com",
33 | },
34 | },
35 | Subject: "Send API testing",
36 | TextPart: "SendMail is working!",
37 | },
38 | },
39 | }
40 | )
41 |
42 | var (
43 | mux *http.ServeMux
44 | server *httptest.Server
45 | client *mailjet.Client
46 | )
47 |
48 | func fakeServer() func() {
49 | mux = http.NewServeMux()
50 | server = httptest.NewServer(mux)
51 | client = mailjet.NewMailjetClient("apiKeyPublic", "apiKeyPrivate", server.URL+"/v3")
52 |
53 | return func() {
54 | server.Close()
55 | }
56 | }
57 |
58 | func handle(path, response string) {
59 | mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
60 | w.Header().Set("Content-Type", "application/json")
61 | w.WriteHeader(http.StatusOK)
62 | fmt.Fprint(w, response)
63 | })
64 | }
65 |
66 | func randSeq(n int) string {
67 | rand.Seed(time.Now().UnixNano())
68 | b := make([]rune, n)
69 | for i := range b {
70 | //nolint:gosec // G404 crypto random is not required here
71 | b[i] = letters[rand.Intn(len(letters))]
72 | }
73 | return string(b)
74 | }
75 |
76 | // NewMockedMailjetClient returns an instance of `Client` with mocked http and smtp clients injected
77 | func newMockedMailjetClient() *mailjet.Client {
78 | httpClientMocked := mailjet.NewhttpClientMock(true)
79 | smtpClientMocked := mailjet.NewSMTPClientMock(true)
80 | client := mailjet.NewClient(httpClientMocked, smtpClientMocked)
81 |
82 | return client
83 | }
84 |
85 | func TestCreateListrecipient(t *testing.T) {
86 | teardown := fakeServer()
87 | defer teardown()
88 |
89 | mux.HandleFunc("/v3/REST/listrecipient", func(w http.ResponseWriter, r *http.Request) {
90 | if r.Method != "POST" {
91 | w.WriteHeader(http.StatusNotFound)
92 | return
93 | }
94 |
95 | b, err := ioutil.ReadAll(r.Body)
96 | if err != nil {
97 | t.Fatal("Unexpected error:", err)
98 | }
99 |
100 | body := make(map[string]interface{})
101 | if err = json.Unmarshal(b, &body); err != nil {
102 | t.Fatal("Invalid body:", err)
103 | }
104 |
105 | _, id := body["ContactID"]
106 | _, alt := body["ContactALT"]
107 | _, listID := body["ListID"]
108 | if !id && !alt || !listID {
109 | w.WriteHeader(http.StatusBadRequest)
110 | fmt.Fprint(w, `{"ErrorMessage": "Missing required parameters"}`)
111 | }
112 | })
113 |
114 | t.Run("successfully create list", func(t *testing.T) {
115 | req := &mailjet.Request{
116 | Resource: "listrecipient",
117 | }
118 | fullRequest := &mailjet.FullRequest{
119 | Info: req,
120 | Payload: resources.Listrecipient{
121 | IsUnsubscribed: true,
122 | ContactID: 124409882,
123 | ContactALT: "joe.doe@mailjet.com",
124 | ListID: 32964,
125 | },
126 | }
127 |
128 | var resp []resources.Listrecipient
129 | err := client.Post(fullRequest, &resp)
130 | if err != nil {
131 | t.Fatal(err)
132 | }
133 | })
134 |
135 | t.Run("failure when required parameters missing", func(t *testing.T) {
136 | req := &mailjet.Request{
137 | Resource: "listrecipient",
138 | }
139 | fullRequest := &mailjet.FullRequest{
140 | Info: req,
141 | Payload: resources.Listrecipient{
142 | IsUnsubscribed: true,
143 | ListID: 32964,
144 | },
145 | }
146 |
147 | var resp []resources.Listrecipient
148 | err := client.Post(fullRequest, &resp)
149 | if err == nil {
150 | t.Fatal("Expected error")
151 | }
152 | })
153 | }
154 |
155 | func TestMessage(t *testing.T) {
156 | teardown := fakeServer()
157 | defer teardown()
158 |
159 | handle("/v3/REST/message", `
160 | {
161 | "Count": 1,
162 | "Data": [
163 | {
164 | "ArrivedAt": "2020-10-08T06:36:35Z",
165 | "AttachmentCount": 0,
166 | "AttemptCount": 0,
167 | "CampaignID": 426400,
168 | "ContactAlt": "",
169 | "ContactID": 124409882,
170 | "Delay": 0,
171 | "DestinationID": 124879,
172 | "FilterTime": 0,
173 | "ID": 94294117474376580,
174 | "IsClickTracked": false,
175 | "IsHTMLPartIncluded": false,
176 | "IsOpenTracked": true,
177 | "IsTextPartIncluded": false,
178 | "IsUnsubTracked": false,
179 | "MessageSize": 810,
180 | "SenderID": 52387,
181 | "SpamassassinScore": 0,
182 | "SpamassRules": "",
183 | "StatePermanent": false,
184 | "Status": "sent",
185 | "Subject": "",
186 | "UUID": "6f66806a-c4d6-4a33-99dc-bedbc7c4217f"
187 | }
188 | ],
189 | "Total": 1
190 | }
191 | `)
192 |
193 | request := &mailjet.Request{
194 | Resource: "message",
195 | }
196 |
197 | var data []resources.Message
198 |
199 | err := client.Get(request, &data)
200 | if err != nil {
201 | t.Fatal(err)
202 | }
203 | }
204 |
205 | func TestMessageinformation(t *testing.T) {
206 | t.Run("empty SpamAssassinRules", func(t *testing.T) {
207 | teardown := fakeServer()
208 | defer teardown()
209 |
210 | handle("/v3/REST/messageinformation", `
211 | {
212 | "Count": 1,
213 | "Data": [
214 | {
215 | "CampaignID": 0,
216 | "ClickTrackedCount": 0,
217 | "ContactID": 124409882,
218 | "CreatedAt": "2020-10-09T06:07:56Z",
219 | "ID": 288230380871887400,
220 | "MessageSize": 434,
221 | "OpenTrackedCount": 0,
222 | "QueuedCount": 0,
223 | "SendEndAt": "2020-10-09T06:07:56Z",
224 | "SentCount": 1602223677,
225 | "SpamAssassinRules": {
226 | "ALT": "",
227 | "ID": -1
228 | },
229 | "SpamAssassinScore": 0
230 | }
231 | ],
232 | "Total": 1
233 | }
234 | `)
235 |
236 | request := &mailjet.Request{
237 | Resource: "messageinformation",
238 | }
239 |
240 | var data []resources.Messageinformation
241 |
242 | err := client.Get(request, &data)
243 | if err != nil {
244 | t.Fatal(err)
245 | }
246 | })
247 |
248 | t.Run("not empty SpamAssassinRules", func(t *testing.T) {
249 | teardown := fakeServer()
250 | defer teardown()
251 |
252 | handle("/v3/REST/messageinformation", `
253 | {
254 | "Count": 1,
255 | "Data": [
256 | {
257 | "CampaignID": 0,
258 | "ClickTrackedCount": 0,
259 | "ContactID": 124409882,
260 | "CreatedAt": "2020-10-09T06:07:56Z",
261 | "ID": 288230380871887400,
262 | "MessageSize": 434,
263 | "OpenTrackedCount": 0,
264 | "QueuedCount": 0,
265 | "SendEndAt": "2020-10-09T06:07:56Z",
266 | "SentCount": 1602223677,
267 | "SpamAssassinRules": {
268 | "ALT": "",
269 | "ID": -1,
270 | "Items": [
271 | {
272 | "ALT": "MISSING_DATE",
273 | "HitCount": 81115,
274 | "ID": 1,
275 | "Name": "MISSING_DATE",
276 | "Score": 2.739
277 | },
278 | {
279 | "ALT": "MISSING_HEADERS",
280 | "HitCount": 48433743,
281 | "ID": 2,
282 | "Name": "MISSING_HEADERS",
283 | "Score": 0.915
284 | }
285 | ]
286 | },
287 | "SpamAssassinScore": 0
288 | }
289 | ],
290 | "Total": 1
291 | }
292 | `)
293 |
294 | request := &mailjet.Request{
295 | Resource: "messageinformation",
296 | }
297 |
298 | var data []resources.Messageinformation
299 |
300 | err := client.Get(request, &data)
301 | if err != nil {
302 | t.Fatal(err)
303 | }
304 | })
305 | }
306 |
307 | func TestUnitList(t *testing.T) {
308 | m := newMockedMailjetClient()
309 |
310 | var data []resources.Sender
311 | count, _, err := m.List("sender", &data)
312 | if err != nil {
313 | t.Fatal("Unexpected error:", err)
314 | }
315 | if count < 1 {
316 | t.Fatal("At least one sender expected !")
317 | }
318 |
319 | httpClientMocked := mailjet.NewhttpClientMock(false)
320 | smtpClientMocked := mailjet.NewSMTPClientMock(true)
321 | cl := mailjet.NewClient(httpClientMocked, smtpClientMocked, "custom")
322 |
323 | _, _, err = cl.List("sender", &data)
324 | if err == nil {
325 | t.Fail()
326 | }
327 | }
328 |
329 | func TestUnitGet(t *testing.T) {
330 | m := newMockedMailjetClient()
331 |
332 | var data []resources.User
333 | resource := "user"
334 | count, _, err := m.List(resource, &data)
335 | if err != nil {
336 | t.Fatal("Unexpected error:", err)
337 | }
338 | if count < 1 {
339 | t.Fatal("At least one user expected !")
340 | }
341 | if data == nil {
342 | t.Fatal("Empty result")
343 | }
344 |
345 | mr := &mailjet.Request{Resource: resource, ID: data[0].ID}
346 | data = make([]resources.User, 0)
347 | err = m.Get(mr, &data)
348 | if err != nil {
349 | t.Fatal("Unexpected error:", err)
350 | }
351 | }
352 |
353 | func TestUnitPost(t *testing.T) {
354 | m := newMockedMailjetClient()
355 |
356 | var data []resources.Contact
357 | rstr := randSeq(10)
358 | t.Logf("Create new contact: \"%s@mailjet.com\"\n", rstr)
359 | fmr := &mailjet.FullRequest{
360 | Info: &mailjet.Request{Resource: "contact"},
361 | Payload: &resources.Contact{Name: rstr, Email: rstr + "@mailjet.com"},
362 | }
363 | err := m.Post(fmr, &data)
364 | if err != nil {
365 | t.Fatal("Unexpected error:", err)
366 | }
367 | if data == nil {
368 | t.Fatal("Empty result")
369 | }
370 | t.Logf("Created contact: %+v\n", data[0])
371 | }
372 |
373 | func TestUnitPut(t *testing.T) {
374 | m := newMockedMailjetClient()
375 |
376 | var data []resources.Contactslist
377 | resource := "contactslist"
378 | count, _, err := m.List(resource, &data)
379 | if err != nil {
380 | t.Fatal("Unexpected error:", err)
381 | }
382 | if count < 1 {
383 | t.Fatal("At least one contact list expected on test account!")
384 | }
385 | if data == nil {
386 | t.Fatal("Empty result")
387 | }
388 |
389 | rstr := randSeq(10)
390 | t.Logf("Update name of the contact list: %s -> %s\n", data[0].Name, rstr)
391 | data[0].Name = randSeq(10)
392 | fmr := &mailjet.FullRequest{
393 | Info: &mailjet.Request{Resource: resource, AltID: data[0].Address},
394 | Payload: data[0],
395 | }
396 | err = m.Put(fmr, []string{"Name"})
397 | if err != nil {
398 | t.Fatal("Unexpected error:", err)
399 | }
400 | }
401 |
402 | func TestUnitDelete(t *testing.T) {
403 | m := newMockedMailjetClient()
404 |
405 | var data []resources.Listrecipient
406 | resource := "listrecipient"
407 | count, _, err := m.List(resource, &data)
408 | if err != nil {
409 | t.Fatal("Unexpected error:", err)
410 | }
411 | if count < 1 {
412 | return
413 | }
414 | if data == nil {
415 | t.Fatal("Empty result")
416 | }
417 |
418 | mr := &mailjet.Request{
419 | ID: data[0].ID,
420 | Resource: resource,
421 | }
422 | err = m.Delete(mr)
423 | if err != nil {
424 | t.Error(err)
425 | }
426 | }
427 |
428 | func TestUnitSendMail(t *testing.T) {
429 | m := newMockedMailjetClient()
430 |
431 | var data []resources.Sender
432 | count, _, err := m.List("sender", &data)
433 | if err != nil {
434 | t.Fatal("Unexpected error:", err)
435 | }
436 | if count < 1 || data == nil {
437 | t.Fatal("At least one sender expected in the test account!")
438 | }
439 |
440 | param := &mailjet.InfoSendMail{
441 | FromEmail: data[0].Email,
442 | FromName: data[0].Name,
443 | Recipients: []mailjet.Recipient{
444 | {
445 | Email: data[0].Email,
446 | },
447 | },
448 | Subject: "Send API testing",
449 | TextPart: "SendMail is working !",
450 | }
451 | _, err = m.SendMail(param)
452 | if err != nil {
453 | t.Fatal("Unexpected error:", err)
454 | }
455 | }
456 |
457 | func TestSendMailV31(t *testing.T) {
458 | tests := []struct {
459 | name string
460 | messages mailjet.MessagesV31
461 | mockResponse interface{}
462 | mockStatusCode int
463 | wantResponse *mailjet.ResultsV31
464 | wantErr interface{}
465 | }{
466 | {
467 | name: "sending successful",
468 | messages: defaultMessages,
469 | mockResponse: mailjet.ResultsV31{
470 | ResultsV31: []mailjet.ResultV31{
471 | {
472 | To: []mailjet.GeneratedMessageV31{
473 | {
474 | Email: "recipient@company.com",
475 | MessageUUID: "ac93d194-1432-4e25-a215-2cb450d4a818",
476 | MessageID: 87,
477 | },
478 | },
479 | },
480 | },
481 | },
482 | mockStatusCode: 200,
483 | wantResponse: &mailjet.ResultsV31{
484 | ResultsV31: []mailjet.ResultV31{
485 | {
486 | To: []mailjet.GeneratedMessageV31{
487 | {
488 | Email: "recipient@company.com",
489 | MessageUUID: "ac93d194-1432-4e25-a215-2cb450d4a818",
490 | MessageID: 87,
491 | },
492 | },
493 | },
494 | },
495 | },
496 | wantErr: nil,
497 | },
498 | {
499 | name: "authorization failed",
500 | messages: defaultMessages,
501 | mockResponse: mailjet.ErrorInfoV31{
502 | Identifier: "ac93d194-1432-4e25-a215-2cb450d4a818",
503 | StatusCode: 401,
504 | Message: "API key authentication/authorization failure. You may be unauthorized to access the API or your API key may be expired. Visit API keys management section to check your keys.",
505 | },
506 | mockStatusCode: 401,
507 | wantResponse: nil,
508 | wantErr: &mailjet.ErrorInfoV31{
509 | Identifier: "ac93d194-1432-4e25-a215-2cb450d4a818",
510 | StatusCode: 401,
511 | Message: "API key authentication/authorization failure. You may be unauthorized to access the API or your API key may be expired. Visit API keys management section to check your keys.",
512 | },
513 | },
514 | {
515 | name: "simple errors in request",
516 | messages: messagesWithAdvancedErrorHandling(),
517 | mockResponse: mailjet.APIFeedbackErrorsV31{
518 | Messages: []mailjet.APIFeedbackErrorV31{
519 | {
520 | Errors: []mailjet.APIErrorDetailsV31{
521 | {
522 | ErrorCode: "mj-0013",
523 | ErrorIdentifier: "ac93d194-1432-4e25-a215-2cb450d4a818",
524 | StatusCode: 400,
525 | // It is not, but let's suppose it is.
526 | ErrorMessage: "\"recipient@company.com\" is an invalid email address.",
527 | ErrorRelatedTo: []string{"To[0].Email"},
528 | },
529 | },
530 | },
531 | },
532 | },
533 | mockStatusCode: 400,
534 | wantResponse: nil,
535 | wantErr: &mailjet.APIFeedbackErrorsV31{
536 | Messages: []mailjet.APIFeedbackErrorV31{
537 | {
538 | Errors: []mailjet.APIErrorDetailsV31{
539 | {
540 | ErrorCode: "mj-0013",
541 | ErrorIdentifier: "ac93d194-1432-4e25-a215-2cb450d4a818",
542 | StatusCode: 400,
543 | // It is not, but let's suppose it is.
544 | ErrorMessage: "\"recipient@company.com\" is an invalid email address.",
545 | ErrorRelatedTo: []string{"To[0].Email"},
546 | },
547 | },
548 | },
549 | },
550 | },
551 | },
552 | {
553 | name: "advanced error handling failed",
554 | messages: messagesWithAdvancedErrorHandling(),
555 | mockResponse: mailjet.APIFeedbackErrorsV31{
556 | Messages: []mailjet.APIFeedbackErrorV31{
557 | {
558 | Errors: []mailjet.APIErrorDetailsV31{
559 | {
560 | ErrorCode: "send-0008",
561 | ErrorIdentifier: "ac93d194-1432-4e25-a215-2cb450d4a818",
562 | StatusCode: 403,
563 | ErrorMessage: "\"passenger@mailjet.com\" is not an authorized sender email address for your account.",
564 | ErrorRelatedTo: []string{"From"},
565 | },
566 | },
567 | },
568 | },
569 | },
570 | mockStatusCode: 403,
571 | wantResponse: nil,
572 | wantErr: &mailjet.APIFeedbackErrorsV31{
573 | Messages: []mailjet.APIFeedbackErrorV31{
574 | {
575 | Errors: []mailjet.APIErrorDetailsV31{
576 | {
577 | ErrorCode: "send-0008",
578 | ErrorIdentifier: "ac93d194-1432-4e25-a215-2cb450d4a818",
579 | StatusCode: 403,
580 | ErrorMessage: "\"passenger@mailjet.com\" is not an authorized sender email address for your account.",
581 | ErrorRelatedTo: []string{"From"},
582 | },
583 | },
584 | },
585 | },
586 | },
587 | },
588 | }
589 |
590 | for _, test := range tests {
591 | t.Run(test.name, func(t *testing.T) {
592 | // TODO(Go1.22+): remove:
593 | messages := test.messages // https://go.dev/wiki/CommonMistakes
594 |
595 | httpClientMocked := mailjet.NewhttpClientMock(true)
596 | httpClientMocked.SendMailV31Func = func(req *http.Request) (*http.Response, error) {
597 | if req.Header.Get("Content-Type") != "application/json" {
598 | t.Errorf("Wanted request content-type header to be: application/json, got: %s", req.Header.Get("Content-Type"))
599 | }
600 |
601 | user, pass, ok := req.BasicAuth()
602 | if !ok || user != httpClientMocked.APIKeyPublic() || pass != httpClientMocked.APIKeyPrivate() {
603 | t.Errorf("Wanted HTTP basic auth to be: %s/%s, got %s/%s", user, pass,
604 | httpClientMocked.APIKeyPublic(), httpClientMocked.APIKeyPrivate())
605 | }
606 |
607 | var msgs mailjet.MessagesV31
608 | err := json.NewDecoder(req.Body).Decode(&msgs)
609 | if err != nil {
610 | t.Fatalf("Could not decode request body and read message information: %v", err)
611 | }
612 |
613 | if !reflect.DeepEqual(test.messages, msgs) {
614 | t.Errorf("Wanted request messages: %+v, got: %+v", test.messages, msgs)
615 | }
616 |
617 | rawBytes, _ := json.Marshal(test.mockResponse)
618 | return &http.Response{
619 | Body: ioutil.NopCloser(bytes.NewBuffer(rawBytes)),
620 | StatusCode: test.mockStatusCode,
621 | }, nil
622 | }
623 |
624 | m := mailjet.NewClient(httpClientMocked, mailjet.NewSMTPClientMock(true))
625 |
626 | res, err := m.SendMailV31(&messages)
627 | if !reflect.DeepEqual(err, test.wantErr) {
628 | t.Fatalf("Wanted error: %+v, got: %+v", err, test.wantErr)
629 | }
630 |
631 | if !reflect.DeepEqual(test.wantResponse, res) {
632 | t.Fatalf("Wanted response: %+v, got %+v", test.wantResponse, res)
633 | }
634 | })
635 | }
636 | }
637 |
638 | func messagesWithAdvancedErrorHandling() mailjet.MessagesV31 {
639 | withAdvancedErrorChecking := defaultMessages
640 | withAdvancedErrorChecking.AdvanceErrorHandling = true
641 | return withAdvancedErrorChecking
642 | }
643 |
--------------------------------------------------------------------------------
/mailjet_resources.go:
--------------------------------------------------------------------------------
1 | package mailjet
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "net/textproto"
7 | "sync"
8 |
9 | "encoding/json"
10 | )
11 |
12 | /*
13 | ** API structures
14 | */
15 |
16 | // Client bundles data needed by a large number
17 | // of methods in order to interact with the Mailjet API.
18 | type Client struct {
19 | apiBase string
20 | httpClient HTTPClientInterface
21 | smtpClient SMTPClientInterface
22 | sync.Mutex
23 | }
24 |
25 | // Request bundles data needed to build the URL.
26 | type Request struct {
27 | Resource string
28 | ID int64
29 | AltID string
30 | Action string
31 | ActionID int64
32 | }
33 |
34 | // DataRequest bundles data needed to build the DATA URL.
35 | type DataRequest struct {
36 | SourceType string
37 | SourceTypeID int64
38 | DataType string
39 | MimeType string
40 | DataTypeID int64
41 | LastID bool
42 | }
43 |
44 | // FullRequest is the same as a Request but with a payload.
45 | type FullRequest struct {
46 | Info *Request
47 | Payload interface{}
48 | }
49 |
50 | // FullDataRequest is the same as a DataRequest but with a payload.
51 | type FullDataRequest struct {
52 | Info *DataRequest
53 | Payload interface{}
54 | }
55 |
56 | // RequestOptions are functional options that modify the specified request.
57 | type RequestOptions func(*http.Request)
58 |
59 | // RequestResult is the JSON result sent by the API.
60 | type RequestResult struct {
61 | Count int
62 | Data interface{}
63 | Total int
64 | }
65 |
66 | // RequestError is the error returned by the API.
67 | type RequestError struct {
68 | ErrorInfo string
69 | ErrorMessage string
70 | StatusCode int
71 | }
72 |
73 | func (e RequestError) Error() string {
74 | return fmt.Sprintf("Unexpected server response code: %d: %s (%s)", e.StatusCode, e.ErrorMessage, e.ErrorInfo)
75 | }
76 |
77 | // RequestErrorV31 is the error returned by the API.
78 | type RequestErrorV31 struct {
79 | ErrorInfo string
80 | ErrorMessage string
81 | StatusCode int
82 | ErrorIdentifier string
83 | }
84 |
85 | /*
86 | ** Send API structures
87 | */
88 |
89 | // InfoSendMail bundles data used by the Send API.
90 | type InfoSendMail struct {
91 | FromEmail string
92 | FromName string
93 | Sender string `json:",omitempty"`
94 | Recipients []Recipient `json:",omitempty"`
95 | To string `json:",omitempty"`
96 | Cc string `json:",omitempty"`
97 | Bcc string `json:",omitempty"`
98 | Subject string
99 | TextPart string `json:"Text-part,omitempty"`
100 | HTMLPart string `json:"Html-part,omitempty"`
101 | Attachments []Attachment `json:",omitempty"`
102 | InlineAttachments []Attachment `json:"Inline_attachments,omitempty"`
103 | MjPrio int `json:"Mj-prio,omitempty"`
104 | MjCampaign string `json:"Mj-campaign,omitempty"`
105 | MjDeduplicateCampaign bool `json:"Mj-deduplicatecampaign,omitempty"`
106 | MjCustomID string `json:"Mj-CustomID,omitempty"`
107 | MjTemplateID string `json:"Mj-TemplateID,omitempty"`
108 | MjTemplateErrorReporting string `json:"MJ-TemplateErrorReporting,omitempty"`
109 | MjTemplateLanguage string `json:"Mj-TemplateLanguage,omitempty"`
110 | MjTemplateErrorDeliver string `json:"MJ-TemplateErrorDeliver,omitempty"`
111 | MjEventPayLoad string `json:"Mj-EventPayLoad,omitempty"`
112 | Headers map[string]string `json:",omitempty"`
113 | Vars interface{} `json:",omitempty"`
114 | Messages []InfoSendMail `json:",omitempty"`
115 | }
116 |
117 | // Recipient bundles data on the target of the mail.
118 | type Recipient struct {
119 | Email string
120 | Name string
121 | Vars interface{} `json:",omitempty"`
122 | }
123 |
124 | // Attachment bundles data on the file attached to the mail.
125 | type Attachment struct {
126 | ContentType string `json:"Content-Type"`
127 | Content string
128 | Filename string
129 | }
130 |
131 | // SentResult is the JSON result sent by the Send API.
132 | type SentResult struct {
133 | Sent []struct {
134 | Email string
135 | MessageID int64
136 | }
137 | }
138 |
139 | /*
140 | ** SMTP mail sending structures
141 | */
142 |
143 | // InfoSMTP contains mandatory informations to send a mail via SMTP.
144 | type InfoSMTP struct {
145 | From string
146 | Recipients []string
147 | Header textproto.MIMEHeader
148 | Content []byte
149 | }
150 |
151 | /*
152 | ** Send API v3.1 structures
153 | */
154 |
155 | // MessagesV31 definition
156 | type MessagesV31 struct {
157 | Info []InfoMessagesV31 `json:"Messages,omitempty"`
158 | AdvanceErrorHandling bool `json:"AdvanceErrorHandling,omitempty"`
159 | SandBoxMode bool `json:",omitempty"`
160 | }
161 |
162 | // InfoMessagesV31 represents the payload input taken by send API v3.1
163 | type InfoMessagesV31 struct {
164 | From *RecipientV31 `json:",omitempty"`
165 | ReplyTo *RecipientV31 `json:",omitempty"`
166 | Sender *RecipientV31 `json:",omitempty"`
167 | To *RecipientsV31 `json:",omitempty"`
168 | Cc *RecipientsV31 `json:",omitempty"`
169 | Bcc *RecipientsV31 `json:",omitempty"`
170 | Attachments *AttachmentsV31 `json:",omitempty"`
171 | InlinedAttachments *InlinedAttachmentsV31 `json:",omitempty"`
172 | Subject string `json:",omitempty"`
173 | TextPart string `json:",omitempty"`
174 | HTMLPart string `json:",omitempty"`
175 | Priority int `json:",omitempty"`
176 | CustomCampaign string `json:",omitempty"`
177 | StatisticsContactsListID int `json:",omitempty"`
178 | MonitoringCategory string `json:",omitempty"`
179 | DeduplicateCampaign bool `json:",omitempty"`
180 | TrackClicks string `json:",omitempty"`
181 | TrackOpens string `json:",omitempty"`
182 | CustomID string `json:",omitempty"`
183 | Variables map[string]interface{} `json:",omitempty"`
184 | EventPayload string `json:",omitempty"`
185 | TemplateID int `json:",omitempty"`
186 | TemplateLanguage bool `json:",omitempty"`
187 | TemplateErrorReporting *RecipientV31 `json:",omitempty"`
188 | TemplateErrorDeliver bool `json:",omitempty"`
189 | Headers map[string]interface{} `json:",omitempty"`
190 | }
191 |
192 | // RecipientV31 struct handle users input
193 | type RecipientV31 struct {
194 | Email string `json:",omitempty"`
195 | Name string `json:",omitempty"`
196 | }
197 |
198 | // RecipientsV31 is a collection of emails
199 | type RecipientsV31 []RecipientV31
200 |
201 | // AttachmentV31 struct represent a content attachment
202 | type AttachmentV31 struct {
203 | ContentType string `json:"ContentType,omitempty"`
204 | Base64Content string `json:"Base64Content,omitempty"`
205 | Filename string `json:"Filename,omitempty"`
206 | }
207 |
208 | // AttachmentsV31 collection
209 | type AttachmentsV31 []AttachmentV31
210 |
211 | // InlinedAttachmentV31 struct represent the content of an inline attachement
212 | type InlinedAttachmentV31 struct {
213 | AttachmentV31 `json:",omitempty"`
214 | ContentID string `json:"ContentID,omitempty"`
215 | }
216 |
217 | // InlinedAttachmentsV31 collection
218 | type InlinedAttachmentsV31 []InlinedAttachmentV31
219 |
220 | // ErrorInfoV31 struct
221 | type ErrorInfoV31 struct {
222 | Identifier string `json:"ErrorIdentifier,omitempty"`
223 | Info string `json:"ErrorInfo"`
224 | Message string `json:"ErrorMessage"`
225 | StatusCode int `json:"StatusCode"`
226 | }
227 |
228 | func (err *ErrorInfoV31) Error() string {
229 | raw, _ := json.Marshal(err)
230 | return string(raw)
231 | }
232 |
233 | // APIErrorDetailsV31 contains the information details describing a specific error
234 | type APIErrorDetailsV31 struct {
235 | // Deprecated: ErrorClass is no longer populated. Use the ErrorCode to
236 | // classify the issue.
237 | ErrorClass string
238 | ErrorCode string
239 | ErrorIdentifier string
240 | ErrorMessage string
241 | ErrorRelatedTo []string
242 | StatusCode int
243 | }
244 |
245 | // APIFeedbackErrorV31 struct is composed of an error definition and the payload associated
246 | type APIFeedbackErrorV31 struct {
247 | Errors []APIErrorDetailsV31
248 | }
249 |
250 | // APIFeedbackErrorsV31 defines the error when a validation error is being sent by the API
251 | type APIFeedbackErrorsV31 struct {
252 | Messages []APIFeedbackErrorV31
253 | }
254 |
255 | func (api *APIFeedbackErrorsV31) Error() string {
256 | raw, _ := json.Marshal(api)
257 | return string(raw)
258 | }
259 |
260 | // GeneratedMessageV31 contains info to retrieve a generated email
261 | type GeneratedMessageV31 struct {
262 | Email string
263 | MessageUUID string
264 | MessageID int64
265 | MessageHref string
266 | }
267 |
268 | // ResultV31 bundles the results of a sent email
269 | type ResultV31 struct {
270 | Status string
271 | CustomID string `json:",omitempty"`
272 | To []GeneratedMessageV31
273 | Cc []GeneratedMessageV31
274 | Bcc []GeneratedMessageV31
275 | }
276 |
277 | // ResultsV31 bundles several results when several mails are sent
278 | type ResultsV31 struct {
279 | ResultsV31 []ResultV31 `json:"Messages"`
280 | }
281 |
--------------------------------------------------------------------------------
/resources/resources.go:
--------------------------------------------------------------------------------
1 | // Package resources provides mailjet resources properties. This is an helper and
2 | // not a mandatory package.
3 | package resources
4 |
5 | import (
6 | "bytes"
7 | "time"
8 | )
9 |
10 | //
11 | // Resources Properties
12 | //
13 |
14 | // Aggregategraphstatistics: Aggregated campaign statistics grouped over intervals.
15 | type Aggregategraphstatistics struct {
16 | BlockedCount float64 `mailjet:"read_only"`
17 | BlockedStdDev float64 `mailjet:"read_only"`
18 | BouncedCount float64 `mailjet:"read_only"`
19 | BouncedStdDev float64 `mailjet:"read_only"`
20 | CampaignAggregateID int `mailjet:"read_only"`
21 | ClickedCount float64 `mailjet:"read_only"`
22 | ClickedStdDev float64 `mailjet:"read_only"`
23 | OpenedCount float64 `mailjet:"read_only"`
24 | OpenedStdDev float64 `mailjet:"read_only"`
25 | RefTimestamp int `mailjet:"read_only"`
26 | SentCount float64 `mailjet:"read_only"`
27 | SentStdDev float64 `mailjet:"read_only"`
28 | SpamComplaintCount float64 `mailjet:"read_only"`
29 | SpamcomplaintStdDev float64 `mailjet:"read_only"`
30 | UnsubscribedCount float64 `mailjet:"read_only"`
31 | UnsubscribedStdDev float64 `mailjet:"read_only"`
32 | }
33 |
34 | // Apikey: Manage your Mailjet API Keys.
35 | // API keys are used as credentials to access the API and SMTP server.
36 | type Apikey struct {
37 | ACL string `json:",omitempty"`
38 | APIKey string `mailjet:"read_only"`
39 | CreatedAt *RFC3339DateTime `mailjet:"read_only"`
40 | ID int64 `mailjet:"read_only"`
41 | IsActive bool `json:",omitempty"`
42 | IsMaster bool `mailjet:"read_only"`
43 | Name string
44 | QuarantineValue int `mailjet:"read_only"`
45 | Runlevel RunLevel `mailjet:"read_only"`
46 | SecretKey string `mailjet:"read_only"`
47 | TrackHost string `mailjet:"read_only"`
48 | UserID int64 `mailjet:"read_only"`
49 | }
50 |
51 | // Apikeyaccess: Access rights description on API keys for subaccounts/users.
52 | type Apikeyaccess struct {
53 | AllowedAccess string `json:",omitempty"`
54 | APIKeyID int64 `json:",omitempty"`
55 | APIKeyALT string `json:",omitempty"`
56 | CreatedAt *RFC3339DateTime `json:",omitempty"`
57 | CustomName string `json:",omitempty"`
58 | ID int64 `mailjet:"read_only"`
59 | IsActive bool `json:",omitempty"`
60 | LastActivityAt *RFC3339DateTime `json:",omitempty"`
61 | RealUserID int64 `json:",omitempty"`
62 | RealUserALT string `json:",omitempty"`
63 | Subaccount *SubAccount `json:",omitempty"`
64 | UserID int64 `json:",omitempty"`
65 | UserALT string `json:",omitempty"`
66 | }
67 |
68 | // Apikeytotals: Global counts for an API Key, since its creation.
69 | type Apikeytotals struct {
70 | BlockedCount int64 `mailjet:"read_only"`
71 | BouncedCount int64 `mailjet:"read_only"`
72 | ClickedCount int64 `mailjet:"read_only"`
73 | DeliveredCount int64 `mailjet:"read_only"`
74 | LastActivity int64 `mailjet:"read_only"`
75 | OpenedCount int64 `mailjet:"read_only"`
76 | ProcessedCount int64 `mailjet:"read_only"`
77 | QueuedCount int64 `mailjet:"read_only"`
78 | SpamcomplaintCount int64 `mailjet:"read_only"`
79 | UnsubscribedCount int64 `mailjet:"read_only"`
80 | }
81 |
82 | // Apitoken: Access token for API, used to give access to an API Key in conjunction with our IFrame API.
83 | type Apitoken struct {
84 | ACL string `json:",omitempty"`
85 | AllowedAccess string
86 | APIKeyID int64 `json:",omitempty"`
87 | APIKeyALT string `json:",omitempty"`
88 | CatchedIP string `json:"CatchedIp,omitempty"`
89 | CreatedAt *RFC3339DateTime `json:",omitempty"`
90 | FirstUsedAt *RFC3339DateTime `json:",omitempty"`
91 | ID int64 `mailjet:"read_only"`
92 | IsActive bool `json:",omitempty"`
93 | Lang string `json:",omitempty"`
94 | LastUsedAt *RFC3339DateTime `json:",omitempty"`
95 | SentData string `json:",omitempty"`
96 | Timezone string `json:",omitempty"`
97 | Token string `json:",omitempty"`
98 | TokenType string
99 | ValidFor int `json:",omitempty"`
100 | }
101 |
102 | // Axtesting: AX testing object
103 | type Axtesting struct {
104 | ContactListID int64 `json:",omitempty"`
105 | ContactListALT string `json:",omitempty"`
106 | CreatedAt *RFC3339DateTime `json:",omitempty"`
107 | Deleted bool `json:",omitempty"`
108 | ID int64 `mailjet:"read_only"`
109 | Mode AXTestMode `json:",omitempty"`
110 | Name string `json:",omitempty"`
111 | Percentage float64 `json:",omitempty"`
112 | RemainderAt *RFC3339DateTime `json:",omitempty"`
113 | SegmentationID int64 `json:",omitempty"`
114 | SegmentationALT string `json:",omitempty"`
115 | Starred bool `json:",omitempty"`
116 | StartAt *RFC3339DateTime `json:",omitempty"`
117 | Status string `json:",omitempty"`
118 | StatusCode int `mailjet:"read_only"`
119 | StatusString string `json:",omitempty"`
120 | WinnerClickRate float64 `json:",omitempty"`
121 | WinnerID int `json:",omitempty"`
122 | WinnerMethod WinnerMethod `json:",omitempty"`
123 | WinnerOpenRate float64 `json:",omitempty"`
124 | WinnerSpamRate float64 `json:",omitempty"`
125 | WinnerUnsubRate float64 `json:",omitempty"`
126 | }
127 |
128 | // Batchjob: Batch jobs running on the Mailjet infrastructure.
129 | type Batchjob struct {
130 | AliveAt int64 `json:",omitempty"`
131 | APIKeyID int64 `json:",omitempty"`
132 | APIKeyALT string `json:",omitempty"`
133 | Blocksize int `json:",omitempty"`
134 | Count int `json:",omitempty"`
135 | Current int `json:",omitempty"`
136 | Data *BaseData
137 | Errcount int `json:",omitempty"`
138 | ErrTreshold int `json:",omitempty"`
139 | ID int64 `mailjet:"read_only"`
140 | JobEnd int64 `json:",omitempty"`
141 | JobStart int64 `json:",omitempty"`
142 | JobType string
143 | Method string `json:",omitempty"`
144 | RefID int64 `json:"RefID,omitempty"`
145 | RequestAt int64 `json:",omitempty"`
146 | Status string `json:",omitempty"`
147 | Throttle int `json:",omitempty"`
148 | }
149 |
150 | type Contactsjob struct {
151 | Count int64
152 | Error string
153 | ErrorFile string
154 | Status string
155 | JobStart string
156 | JobEnd string
157 | }
158 |
159 | // Bouncestatistics: Statistics on the bounces generated by emails sent on a given API Key.
160 | type Bouncestatistics struct {
161 | BouncedAt *RFC3339DateTime `mailjet:"read_only"`
162 | CampaignID int64 `mailjet:"read_only"`
163 | CampaignALT string `mailjet:"read_only"`
164 | ContactID int64 `mailjet:"read_only"`
165 | ContactALT string `mailjet:"read_only"`
166 | ID int64 `mailjet:"read_only"`
167 | IsBlocked bool `mailjet:"read_only"`
168 | IsStatePermanent bool `mailjet:"read_only"`
169 | StateID int64 `mailjet:"read_only"`
170 | }
171 |
172 | // Campaign: Historical view of sent emails, both transactional and marketing.
173 | // Each e-mail going through Mailjet is attached to a Campaign.
174 | // This object is automatically generated by Mailjet.
175 | type Campaign struct {
176 | CampaignType int `mailjet:"read_only"`
177 | ClickTracked int64 `mailjet:"read_only"`
178 | CreatedAt *RFC3339DateTime `mailjet:"read_only"`
179 | CustomValue string `mailjet:"read_only"`
180 | FirstMessageID int64 `mailjet:"read_only"`
181 | FromID int64 `mailjet:"read_only"`
182 | FromALT string `mailjet:"read_only"`
183 | FromEmail string `mailjet:"read_only"`
184 | FromName string `mailjet:"read_only"`
185 | HasHtmlCount int64 `mailjet:"read_only"`
186 | HasTxtCount int64 `mailjet:"read_only"`
187 | ID int64 `mailjet:"read_only"`
188 | IsDeleted bool `json:",omitempty"`
189 | IsStarred bool `json:",omitempty"`
190 | ListID int64 `mailjet:"read_only"`
191 | ListALT string `mailjet:"read_only"`
192 | NewsLetterID int64 `mailjet:"read_only"`
193 | OpenTracked int64 `mailjet:"read_only"`
194 | SegmentationID int64 `mailjet:"read_only"`
195 | SegmentationALT string `mailjet:"read_only"`
196 | SendEndAt *RFC3339DateTime `mailjet:"read_only"`
197 | SendStartAt *RFC3339DateTime `mailjet:"read_only"`
198 | SpamassScore float64 `mailjet:"read_only"`
199 | Status string `mailjet:"read_only"`
200 | Subject string `mailjet:"read_only"`
201 | UnsubscribeTrackedCount int64 `mailjet:"read_only"`
202 | }
203 |
204 | // Campaignaggregate: User defined campaign aggregates
205 | type Campaignaggregate struct {
206 | CampaignIDS string `json:",omitempty"`
207 | ContactFilterID int64 `json:",omitempty"`
208 | ContactFilterALT string `json:",omitempty"`
209 | ContactsListID int64 `json:",omitempty"`
210 | ContactsListALT string `json:",omitempty"`
211 | Final bool `mailjet:"read_only"`
212 | FromDate *RFC3339DateTime `json:",omitempty"`
213 | ID int64 `mailjet:"read_only"`
214 | Keyword string `json:",omitempty"`
215 | Name string `json:",omitempty"`
216 | SenderID int64 `json:",omitempty"`
217 | SenderALT string `json:",omitempty"`
218 | ToDate *RFC3339DateTime `json:",omitempty"`
219 | }
220 |
221 | // Campaigndraft: Newsletter and CampaignDraft objects are differentiated by the EditMode values.
222 | type Campaigndraft struct {
223 | AXFractionName string `json:",omitempty"`
224 | AXTesting *Axtesting `json:",omitempty"`
225 | CampaignID int64 `json:",omitempty"`
226 | CampaignALT string `json:",omitempty"`
227 | ContactsListID int64 `json:",omitempty"`
228 | ContactsListALT string `json:",omitempty"`
229 | CreatedAt *RFC3339DateTime `json:",omitempty"`
230 | Current int64 `json:",omitempty"`
231 | DeliveredAt *RFC3339DateTime `json:",omitempty"`
232 | EditMode string `json:",omitempty"`
233 | ID int64 `mailjet:"read_only"`
234 | IsStarred bool `json:",omitempty"`
235 | IsTextPartIncluded bool `json:",omitempty"`
236 | Locale string
237 | ModifiedAt *RFC3339DateTime `json:",omitempty"`
238 | Preset string `json:",omitempty"`
239 | ReplyEmail string `json:",omitempty"`
240 | SegmentationID int64 `json:",omitempty"`
241 | SegmentationALT string `json:",omitempty"`
242 | Sender string
243 | SenderEmail string
244 | SenderName string `json:",omitempty"`
245 | Status int64 `mailjet:"read_only"`
246 | Subject string
247 | TemplateID int64 `json:",omitempty"`
248 | TemplateALT string `json:",omitempty"`
249 | Title string `json:",omitempty"`
250 | URL string `json:"Url,omitempty"`
251 | Used bool `json:",omitempty"`
252 | }
253 |
254 | // CampaigndraftSchedule:
255 | type CampaigndraftSchedule struct {
256 | Date *RFC3339DateTime
257 | }
258 |
259 | // CampaigndraftTest:
260 | type CampaigndraftTest struct {
261 | Recipients []Recipient
262 | }
263 |
264 | // CampaigndraftDetailcontent:
265 | type CampaigndraftDetailcontent struct {
266 | TextPart string `json:"Text-part,omitempty"`
267 | HtmlPart string `json:"Html-part,omitempty"`
268 | MJMLContent string `json:",omitempty"`
269 | Headers interface{} `json:",omitempty"`
270 | }
271 |
272 | // Campaigngraphstatistics: API Campaign statistics grouped over intervals
273 | type Campaigngraphstatistics struct {
274 | Clickcount int64 `mailjet:"read_only"`
275 | ID int64 `mailjet:"read_only"`
276 | Opencount int64 `mailjet:"read_only"`
277 | Spamcount int64 `mailjet:"read_only"`
278 | Tick int64 `mailjet:"read_only"`
279 | Unsubcount int64 `mailjet:"read_only"`
280 | }
281 |
282 | // Campaignoverview: Returns a list of campaigns, including the AX campaigns
283 | type Campaignoverview struct {
284 | ClickedCount int64 `mailjet:"read_only"`
285 | DeliveredCount int64 `mailjet:"read_only"`
286 | EditMode string `mailjet:"read_only"`
287 | EditType string `mailjet:"read_only"`
288 | ID int64 `mailjet:"read_only"`
289 | IDType string `mailjet:"read_only"`
290 | OpenedCount int64 `mailjet:"read_only"`
291 | ProcessedCount int64 `mailjet:"read_only"`
292 | SendTimeStart int64 `mailjet:"read_only"`
293 | Starred bool `mailjet:"read_only"`
294 | Status int `mailjet:"read_only"`
295 | Subject string `mailjet:"read_only"`
296 | Title string `mailjet:"read_only"`
297 | }
298 |
299 | // Campaignstatistics: Statistics related to emails processed by Mailjet, grouped in a Campaign.
300 | type Campaignstatistics struct {
301 | AXTesting *Axtesting `mailjet:"read_only"`
302 | BlockedCount int64 `mailjet:"read_only"`
303 | BouncedCount int64 `mailjet:"read_only"`
304 | CampaignID int64 `mailjet:"read_only"`
305 | CampaignALT string `mailjet:"read_only"`
306 | CampaignIsStarred bool `mailjet:"read_only"`
307 | CampaignSendStartAt *RFC3339DateTime `mailjet:"read_only"`
308 | CampaignSubject string `mailjet:"read_only"`
309 | ClickedCount int64 `mailjet:"read_only"`
310 | ContactListName string `mailjet:"read_only"`
311 | DeliveredCount int64 `mailjet:"read_only"`
312 | LastActivityAt *RFC3339DateTime `mailjet:"read_only"`
313 | NewsLetterID int64 `mailjet:"read_only"`
314 | OpenedCount int64 `mailjet:"read_only"`
315 | ProcessedCount int64 `mailjet:"read_only"`
316 | QueuedCount int64 `mailjet:"read_only"`
317 | SegmentName string `mailjet:"read_only"`
318 | SpamComplaintCount int64 `mailjet:"read_only"`
319 | UnsubscribedCount int64 `mailjet:"read_only"`
320 | }
321 |
322 | // Clickstatistics: Click statistics for messages.
323 | type Clickstatistics struct {
324 | ClickedAt string `mailjet:"read_only"`
325 | ClickedDelay int64 `mailjet:"read_only"`
326 | ContactID int64 `mailjet:"read_only"`
327 | ContactALT string `mailjet:"read_only"`
328 | ID int64 `mailjet:"read_only"`
329 | MessageID int64 `mailjet:"read_only"`
330 | URL string `json:"Url" mailjet:"read_only"`
331 | UserAgent string `mailjet:"read_only"`
332 | }
333 |
334 | // Contact: Manage the details of a Contact.
335 | type Contact struct {
336 | CreatedAt *RFC3339DateTime `mailjet:"read_only"`
337 | DeliveredCount int64 `mailjet:"read_only"`
338 | Email string
339 | ID int64 `mailjet:"read_only"`
340 | IsOptInPending bool `mailjet:"read_only"`
341 | IsSpamComplaining bool `mailjet:"read_only"`
342 | IsExcludedFromCampaigns bool
343 | LastActivityAt *RFC3339DateTime `mailjet:"read_only"`
344 | LastUpdateAt *RFC3339DateTime `mailjet:"read_only"`
345 | Name string `json:",omitempty"`
346 | UnsubscribedAt *RFC3339DateTime `mailjet:"read_only"`
347 | UnsubscribedBy string `mailjet:"read_only"`
348 | }
349 |
350 | // ContactManagecontactslists: Managing the lists for a single contact. POST is supported.
351 | type ContactManagecontactslists struct {
352 | ContactsLists []ContactsListAction
353 | }
354 |
355 | // ContactManagemanycontacts: Uploading many contacts and returns a job_id.
356 | // To monitor the upload issue a GET request to: APIBASEURL/contact/managemanycontacts/:job_id
357 | type ContactManagemanycontacts struct {
358 | ContactsLists []ContactsListAction
359 | Contacts []AddContactAction
360 | }
361 |
362 | // Contactdata: This resource can be used to examine and manipulate the associated extra static data of a contact.
363 | type Contactdata struct {
364 | ContactID int64 `json:",omitempty"`
365 | Data KeyValueList `json:",omitempty"`
366 | ID int64 `mailjet:"read_only"`
367 | }
368 |
369 | // Contactfilter: A list of filter expressions for use in newsletters.
370 | type Contactfilter struct {
371 | Description string `json:",omitempty"`
372 | Expression string `json:",omitempty"`
373 | ID int64 `mailjet:"read_only"`
374 | Name string `json:",omitempty"`
375 | Status string `json:",omitempty"`
376 | }
377 |
378 | // Contacthistorydata: This resource can be used to examine the associated extra historical data of a contact.
379 | type Contacthistorydata struct {
380 | ContactID int64 `json:",omitempty"`
381 | ContactALT string `json:",omitempty"`
382 | CreatedAt *RFC3339DateTime `json:",omitempty"`
383 | Data string `json:",omitempty"`
384 | ID int64 `mailjet:"read_only"`
385 | Name string `json:",omitempty"`
386 | }
387 |
388 | // Contactmetadata: Definition of available extra data items for contacts.
389 | type Contactmetadata struct {
390 | Datatype string
391 | ID int64 `mailjet:"read_only"`
392 | Name string
393 | NameSpace string `mailjet:"read_only"`
394 | }
395 |
396 | // Contactslist: Manage your contact lists. One Contact might be associated to one or more ContactsList.
397 | type Contactslist struct {
398 | Address string `mailjet:"read_only"`
399 | CreatedAt *RFC3339DateTime `mailjet:"read_only"`
400 | ID int64 `mailjet:"read_only"`
401 | IsDeleted bool `json:",omitempty"`
402 | Name string `json:",omitempty"`
403 | SubscriberCount int `mailjet:"read_only"`
404 | }
405 |
406 | // ContactslistManageContact: An action for adding a contact to a contact list.
407 | // The API will internally create the new contact if it does not exist, add or update the name and properties.
408 | // The properties have to be defined before they can be used.
409 | // The API then adds the contact to the contact list with active=true and unsub=specified value
410 | // if it is not already in the list, or updates the entry with these values.
411 | // On success, the API returns a packet with the same format but with all properties available for that contact.
412 | // Only POST is supported.
413 | type ContactslistManageContact struct {
414 | Email string
415 | Name string
416 | Action string
417 | Properties JSONObject
418 | }
419 |
420 | // ContactslistManageManyContacts: Multiple contacts can be uploaded asynchronously using that action.
421 | // Only POST is supported.
422 | type ContactslistManageManyContacts struct {
423 | Action string
424 | Contacts []AddContactAction
425 | }
426 |
427 | type Job struct {
428 | JobID int64
429 | }
430 |
431 | // ContactslistImportList: Import the contacts of another contact list into the current list and apply the specified action on imported contacts.
432 | // In case of conflict, the contact original subscription state is overridden. Returns the ID of the job to monitor.
433 | type ContactslistImportList struct {
434 | Action string
435 | ListID int64
436 | }
437 |
438 | // Contactslistsignup: Contacts list signup request.
439 | type Contactslistsignup struct {
440 | ConfirmAt int64 `json:",omitempty"`
441 | ConfirmIP string `json:"ConfirmIp"`
442 | ContactID int64 `json:",omitempty"`
443 | ContactALT string `json:",omitempty"`
444 | Email string
445 | ID int64 `mailjet:"read_only"`
446 | ListID int64 `json:",omitempty"`
447 | ListALT string `json:",omitempty"`
448 | SignupAt int64 `json:",omitempty"`
449 | SignupIP string `json:"SignupIp,omitempty"`
450 | SignupKey string `json:",omitempty"`
451 | Source string
452 | SourceId int64 `json:",omitempty"`
453 | }
454 |
455 | // Contactstatistics: View message statistics for a given contact.
456 | type Contactstatistics struct {
457 | BlockedCount int64 `mailjet:"read_only"`
458 | BouncedCount int64 `mailjet:"read_only"`
459 | ClickedCount int64 `mailjet:"read_only"`
460 | ContactID int64 `mailjet:"read_only"`
461 | ContactALT string `mailjet:"read_only"`
462 | DeliveredCount int64 `mailjet:"read_only"`
463 | LastActivityAt *RFC3339DateTime `mailjet:"read_only"`
464 | MarketingContacts int64 `mailjet:"read_only"`
465 | OpenedCount int64 `mailjet:"read_only"`
466 | ProcessedCount int64 `mailjet:"read_only"`
467 | QueuedCount int64 `mailjet:"read_only"`
468 | SpamComplaintCount int64 `mailjet:"read_only"`
469 | UnsubscribedCount int64 `mailjet:"read_only"`
470 | UserMarketingContacts int64 `mailjet:"read_only"`
471 | }
472 |
473 | // ContactGetcontactslists: retrieve all contact lists for a specific contact
474 | type ContactGetcontactslists struct {
475 | ListID int64 `mailjet:"read_only"`
476 | IsUnsub bool `mailjet:"read_only"`
477 | IsActive bool `mailjet:"read_only"`
478 | SubscribedAt *RFC3339DateTime `mailjet:"read_only"`
479 | }
480 |
481 | // Csvimport: A wrapper for the CSV importer
482 | type Csvimport struct {
483 | AliveAt *RFC3339DateTime `mailjet:"read_only"`
484 | ContactsListID int64 `json:",omitempty"`
485 | ContactsListALT string `json:",omitempty"`
486 | Count int `mailjet:"read_only"`
487 | Current int `mailjet:"read_only"`
488 | DataID int64
489 | Errcount int `mailjet:"read_only"`
490 | ErrTreshold int `json:",omitempty"`
491 | ID int64 `mailjet:"read_only"`
492 | ImportOptions string `json:",omitempty"`
493 | JobEnd *RFC3339DateTime `mailjet:"read_only"`
494 | JobStart *RFC3339DateTime `mailjet:"read_only"`
495 | Method string `json:",omitempty"`
496 | RequestAt *RFC3339DateTime `mailjet:"read_only"`
497 | Status string `json:",omitempty"`
498 | }
499 |
500 | // Dns: Sender Domain properties.
501 | type Dns struct {
502 | DKIMRecordName string `mailjet:"read_only"`
503 | DKIMRecordValue string `mailjet:"read_only"`
504 | DKIMStatus string `mailjet:"read_only"`
505 | Domain string `mailjet:"read_only"`
506 | ID int64 `mailjet:"read_only"`
507 | IsCheckInProgress bool `mailjet:"read_only"`
508 | LastCheckAt *RFC3339DateTime `mailjet:"read_only"`
509 | OwnerShipToken string `mailjet:"read_only"`
510 | OwnerShipTokenRecordName string `mailjet:"read_only"`
511 | SPFRecordValue string `mailjet:"read_only"`
512 | SPFStatus string `mailjet:"read_only"`
513 | }
514 |
515 | type DnsCheck struct {
516 | DKIMErrors []string `mailjet:"read_only"`
517 | DKIMStatus string `mailjet:"read_only"`
518 | DKIMRecordCurrentValue string `mailjet:"read_only"`
519 | SPFRecordCurrentValue string `mailjet:"read_only"`
520 | SPFErrors []string `mailjet:"read_only"`
521 | SPFStatus string `mailjet:"read_only"`
522 | }
523 |
524 | // Domainstatistics: View Campaign/Message/Click statistics grouped per domain.
525 | type Domainstatistics struct {
526 | BlockedCount int64 `mailjet:"read_only"`
527 | BouncedCount int64 `mailjet:"read_only"`
528 | ClickedCount int64 `mailjet:"read_only"`
529 | DeliveredCount int64 `mailjet:"read_only"`
530 | Domain string `mailjet:"read_only"`
531 | ID int64 `mailjet:"read_only"`
532 | OpenedCount int64 `mailjet:"read_only"`
533 | ProcessedCount int64 `mailjet:"read_only"`
534 | QueuedCount int64 `mailjet:"read_only"`
535 | SpamComplaintCount int64 `mailjet:"read_only"`
536 | UnsubscribedCount int64 `mailjet:"read_only"`
537 | }
538 |
539 | // Eventcallbackurl: Manage event-driven callback URLs, also called webhooks,
540 | // used by the Mailjet platform when a specific action is triggered
541 | type Eventcallbackurl struct {
542 | APIKeyID int64 `json:",omitempty"`
543 | APIKeyALT string `json:",omitempty"`
544 | EventType string `json:",omitempty"`
545 | ID int64 `mailjet:"read_only"`
546 | IsBackup bool `json:",omitempty"`
547 | Status string `json:",omitempty"`
548 | URL string `json:"Url"`
549 | Version int `json:",omitempty"`
550 | }
551 |
552 | // Geostatistics: Message click/open statistics grouped per country
553 | type Geostatistics struct {
554 | ClickedCount int64 `mailjet:"read_only"`
555 | Country string `mailjet:"read_only"`
556 | OpenedCount int64 `mailjet:"read_only"`
557 | }
558 |
559 | // Graphstatistics: API Campaign/message/click statistics grouped over intervals.
560 | type Graphstatistics struct {
561 | BlockedCount int64 `mailjet:"read_only"`
562 | BouncedCount int64 `mailjet:"read_only"`
563 | ClickedCount int64 `mailjet:"read_only"`
564 | DeliveredCount int64 `mailjet:"read_only"`
565 | OpenedCount int64 `mailjet:"read_only"`
566 | ProcessedCount int64 `mailjet:"read_only"`
567 | QueuedCount int64 `mailjet:"read_only"`
568 | RefTimestamp string `mailjet:"read_only"`
569 | SendtimeStart int64 `mailjet:"read_only"`
570 | SpamcomplaintCount int64 `mailjet:"read_only"`
571 | UnsubscribedCount int64 `mailjet:"read_only"`
572 | }
573 |
574 | // Listrecipient: Manage the relationship between a contact and a contactslists.
575 | type Listrecipient struct {
576 | ContactID int64 `json:",omitempty"`
577 | ContactALT string `json:",omitempty"`
578 | ID int64 `mailjet:"read_only"`
579 | IsActive bool `json:",omitempty"`
580 | IsUnsubscribed bool `json:",omitempty"`
581 | ListID int64 `json:",omitempty"`
582 | ListALT string `json:",omitempty"`
583 | UnsubscribedAt *RFC3339DateTime `json:",omitempty"`
584 | }
585 |
586 | // Listrecipientstatistics: View statistics on Messages sent to the recipients of a given list.
587 | type Listrecipientstatistics struct {
588 | BlockedCount int64 `mailjet:"read_only"`
589 | BouncedCount int64 `mailjet:"read_only"`
590 | ClickedCount int64 `mailjet:"read_only"`
591 | Data KeyValueList `mailjet:"read_only"`
592 | DeliveredCount int64 `mailjet:"read_only"`
593 | LastActivityAt *RFC3339DateTime `mailjet:"read_only"`
594 | ListRecipientID int64 `mailjet:"read_only"`
595 | OpenedCount int64 `mailjet:"read_only"`
596 | ProcessedCount int64 `mailjet:"read_only"`
597 | QueuedCount int64 `mailjet:"read_only"`
598 | SpamComplaintCount int64 `mailjet:"read_only"`
599 | UnsubscribedCount int64 `mailjet:"read_only"`
600 | }
601 |
602 | // Liststatistics: View Campaign/message/click statistics grouped by ContactsList.
603 | type Liststatistics struct {
604 | ActiveCount int64 `mailjet:"read_only"`
605 | ActiveUnsubscribedCount int64 `mailjet:"read_only"`
606 | Address string `mailjet:"read_only"`
607 | BlockedCount int64 `mailjet:"read_only"`
608 | BouncedCount int64 `mailjet:"read_only"`
609 | ClickedCount int64 `mailjet:"read_only"`
610 | CreatedAt *RFC3339DateTime `mailjet:"read_only"`
611 | DeliveredCount int64 `mailjet:"read_only"`
612 | ID int64 `mailjet:"read_only"`
613 | IsDeleted bool `mailjet:"read_only"`
614 | LastActivityAt *RFC3339DateTime `mailjet:"read_only"`
615 | Name string `mailjet:"read_only"`
616 | OpenedCount int64 `mailjet:"read_only"`
617 | SpamComplaintCount int64 `mailjet:"read_only"`
618 | SubscriberCount int `mailjet:"read_only"`
619 | UnsubscribedCount int64 `mailjet:"read_only"`
620 | }
621 |
622 | // Message: Allows you to list and view the details of a Message (an e-mail) processed by Mailjet
623 | type Message struct {
624 | ArrivedAt *RFC3339DateTime `json:",omitempty"`
625 | AttachmentCount int `json:",omitempty"`
626 | AttemptCount int `json:",omitempty"`
627 | CampaignID int64 `json:",omitempty"`
628 | CampaignALT string `json:",omitempty"`
629 | ContactID int64 `json:",omitempty"`
630 | ContactALT string `json:",omitempty"`
631 | Delay float64 `json:",omitempty"`
632 | Destination Destination
633 | FilterTime int `json:",omitempty"`
634 | FromID int64 `json:",omitempty"`
635 | FromALT string `json:",omitempty"`
636 | ID int64 `mailjet:"read_only"`
637 | IsClickTracked bool `json:",omitempty"`
638 | IsHTMLPartIncluded bool `json:",omitempty"`
639 | IsOpenTracked bool `json:",omitempty"`
640 | IsTextPartIncluded bool `json:",omitempty"`
641 | IsUnsubTracked bool `json:",omitempty"`
642 | MessageSize int64 `json:",omitempty"`
643 | SpamassassinScore float64 `json:",omitempty"`
644 | SpamassRules string `json:",omitempty"`
645 | Subject string `json:",omitempty"`
646 | StateID int64 `json:",omitempty"`
647 | StatePermanent bool `json:",omitempty"`
648 | Status string `json:",omitempty"`
649 | }
650 |
651 | // Messagehistory: Event history of a message.
652 | type Messagehistory struct {
653 | Comment string `mailjet:"read_only"`
654 | EventAt int64 `mailjet:"read_only"`
655 | EventType string `mailjet:"read_only"`
656 | State string `mailjet:"read_only"`
657 | UserAgent string `json:"Useragent" mailjet:"read_only"`
658 | }
659 |
660 | // Messageinformation: API Key campaign/message information.
661 | type Messageinformation struct {
662 | CampaignID int64 `mailjet:"read_only"`
663 | CampaignALT string `mailjet:"read_only"`
664 | ClickTrackedCount int64 `mailjet:"read_only"`
665 | ContactID int64 `mailjet:"read_only"`
666 | ContactALT string `mailjet:"read_only"`
667 | CreatedAt *RFC3339DateTime `mailjet:"read_only"`
668 | ID int64 `mailjet:"read_only"`
669 | MessageSize int64 `mailjet:"read_only"`
670 | OpenTrackedCount int64 `mailjet:"read_only"`
671 | QueuedCount int64 `mailjet:"read_only"`
672 | SendEndAt *RFC3339DateTime `mailjet:"read_only"`
673 | SentCount int64 `mailjet:"read_only"`
674 | SpamAssassinRules struct {
675 | Items []SpamAssassinRule
676 | } `mailjet:"read_only"`
677 | SpamAssassinScore float64 `mailjet:"read_only"`
678 | }
679 |
680 | // Messagesentstatistics: API Key Statistical campaign/message data.
681 | type Messagesentstatistics struct {
682 | ArrivalTs *RFC3339DateTime `mailjet:"read_only"`
683 | Blocked bool `mailjet:"read_only"`
684 | Bounce bool `mailjet:"read_only"`
685 | BounceDate *RFC3339DateTime `mailjet:"read_only"`
686 | BounceReason string `mailjet:"read_only"`
687 | CampaignID int64 `mailjet:"read_only"`
688 | CampaignALT string `mailjet:"read_only"`
689 | Click bool `mailjet:"read_only"`
690 | CntRecipients int64 `mailjet:"read_only"`
691 | ComplaintDate *RFC3339DateTime `mailjet:"read_only"`
692 | ContactID int64 `mailjet:"read_only"`
693 | ContactALT string `mailjet:"read_only"`
694 | Details string `mailjet:"read_only"`
695 | FBLSource string `mailjet:"read_only"`
696 | MessageID int64 `mailjet:"read_only"`
697 | Open bool `mailjet:"read_only"`
698 | Queued bool `mailjet:"read_only"`
699 | Sent bool `mailjet:"read_only"`
700 | Spam bool `mailjet:"read_only"`
701 | StateID int64 `mailjet:"read_only"`
702 | StatePermanent bool `mailjet:"read_only"`
703 | Status string `mailjet:"read_only"`
704 | ToEmail string `mailjet:"read_only"`
705 | Unsub bool `mailjet:"read_only"`
706 | }
707 |
708 | // Messagestate: Message state reference.
709 | type Messagestate struct {
710 | ID int64 `mailjet:"read_only"`
711 | RelatedTo string `json:",omitempty"`
712 | State string
713 | }
714 |
715 | // MessageStatistics: API key Campaign/Message statistics.
716 | type MessageStatistics struct {
717 | AverageClickDelay float64 `mailjet:"read_only"`
718 | AverageClickedCount float64 `mailjet:"read_only"`
719 | AverageOpenDelay float64 `mailjet:"read_only"`
720 | AverageOpenedCount float64 `mailjet:"read_only"`
721 | BlockedCount int64 `mailjet:"read_only"`
722 | BouncedCount int64 `mailjet:"read_only"`
723 | CampaignCount int64 `mailjet:"read_only"`
724 | ClickedCount int64 `mailjet:"read_only"`
725 | DeliveredCount int64 `mailjet:"read_only"`
726 | OpenedCount int64 `mailjet:"read_only"`
727 | ProcessedCount int64 `mailjet:"read_only"`
728 | QueuedCount int64 `mailjet:"read_only"`
729 | SpamComplaintCount int64 `mailjet:"read_only"`
730 | TransactionalCount int64 `mailjet:"read_only"`
731 | UnsubscribedCount int64 `mailjet:"read_only"`
732 | }
733 |
734 | // Metadata: Mailjet API meta data.
735 | type Metadata struct {
736 | APIVersion string `mailjet:"read_only"`
737 | Actions []ResourceAction `mailjet:"read_only"`
738 | Description string `mailjet:"read_only"`
739 | Filters []ResourceFilter `mailjet:"read_only"`
740 | IsReadOnly bool `mailjet:"read_only"`
741 | Name string `mailjet:"read_only"`
742 | Properties []ResourceProperty `mailjet:"read_only"`
743 | PublicOperations string `mailjet:"read_only"`
744 | SortInfo []struct {
745 | AllowDescending bool `mailjet:"read_only"`
746 | PropertyName string `mailjet:"read_only"`
747 | } `mailjet:"read_only"`
748 | UniqueKey string `mailjet:"read_only"`
749 | }
750 |
751 | type ResourceAction struct {
752 | Description string `mailjet:"read_only"`
753 | IsGlobalAction bool `mailjet:"read_only"`
754 | Name string `mailjet:"read_only"`
755 | Parameters []ResourceFilter `mailjet:"read_only"`
756 | Properties []ResourceProperty `mailjet:"read_only"`
757 | PublicOperations string `mailjet:"read_only"`
758 | }
759 |
760 | type ResourceFilter struct {
761 | DataType string `mailjet:"read_only"`
762 | DefaultValue string `mailjet:"read_only"`
763 | Description string `mailjet:"read_only"`
764 | IsRequired bool `mailjet:"read_only"`
765 | Name string `mailjet:"read_only"`
766 | ReadOnly bool `mailjet:"read_only"`
767 | }
768 |
769 | type ResourceProperty struct {
770 | DataType string `mailjet:"read_only"`
771 | DefaultValue string `mailjet:"read_only"`
772 | Description string `mailjet:"read_only"`
773 | IsRequired bool `mailjet:"read_only"`
774 | Name string `mailjet:"read_only"`
775 | ReadOnly bool `mailjet:"read_only"`
776 | }
777 |
778 | // Metasender: Management of domains used for sending messages.
779 | // A domain or address must be registered and validated before being used.
780 | // See the related Sender object if you wish to register a given e-mail address.
781 | type Metasender struct {
782 | CreatedAt *RFC3339DateTime `json:",omitempty"`
783 | Description string `json:",omitempty"`
784 | Email string
785 | Filename string `mailjet:"read_only"`
786 | ID int64 `mailjet:"read_only"`
787 | IsEnabled bool `json:",omitempty"`
788 | }
789 |
790 | // Myprofile: Manage user profile data such as address, payment information etc.
791 | type Myprofile struct {
792 | AddressCity string `json:",omitempty"`
793 | AddressCountry string `json:",omitempty"`
794 | AddressPostalCode string `json:",omitempty"`
795 | AddressState string `json:",omitempty"`
796 | AddressStreet string `json:",omitempty"`
797 | BillingEmail string `json:",omitempty"`
798 | BirthdayAt *RFC3339DateTime `json:",omitempty"`
799 | CompanyName string `json:",omitempty"`
800 | CompanyNumOfEmployees string `json:",omitempty"`
801 | ContactPhone string `json:",omitempty"`
802 | EstimatedVolume int `json:",omitempty"`
803 | Features string `json:",omitempty"`
804 | Firstname string `json:",omitempty"`
805 | ID int64 `mailjet:"read_only"`
806 | Industry string `json:",omitempty"`
807 | JobTitle string `json:",omitempty"`
808 | Lastname string `json:",omitempty"`
809 | UserID int64 `json:",omitempty"`
810 | UserALT string `json:",omitempty"`
811 | VAT float64 `mailjet:"read_only"`
812 | VATNumber string `json:",omitempty"`
813 | Website string `json:",omitempty"`
814 | }
815 |
816 | // Newsletter: Newsletter data.
817 | type Newsletter struct {
818 | AXFraction float64 `json:",omitempty"`
819 | AXFractionName string `json:",omitempty"`
820 | AXTesting *Axtesting `json:",omitempty"`
821 | Callback string `json:",omitempty"`
822 | CampaignID int64 `mailjet:"read_only"`
823 | CampaignALT string `mailjet:"read_only"`
824 | ContactsListID int64 `json:",omitempty"`
825 | ContactsListALT string `json:",omitempty"`
826 | CreatedAt *RFC3339DateTime `json:",omitempty"`
827 | DeliveredAt *RFC3339DateTime `json:",omitempty"`
828 | EditMode string `json:",omitempty"`
829 | EditType string `json:",omitempty"`
830 | Footer string `json:",omitempty"`
831 | FooterAddress string `json:",omitempty"`
832 | FooterWYSIWYGType int `json:",omitempty"`
833 | HeaderFilename string `json:",omitempty"`
834 | HeaderLink string `json:",omitempty"`
835 | HeaderText string `json:",omitempty"`
836 | HeaderURL string `json:"HeaderUrl,omitempty"`
837 | ID int64 `mailjet:"read_only"`
838 | IP string `json:"Ip,omitempty"`
839 | IsHandled bool `json:",omitempty"`
840 | IsStarred bool `json:",omitempty"`
841 | IsTextPartIncluded bool `json:",omitempty"`
842 | Locale string
843 | ModifiedAt *RFC3339DateTime `json:",omitempty"`
844 | Permalink string `json:",omitempty"`
845 | PermalinkHost string `json:",omitempty"`
846 | PermalinkWYSIWYGType int `json:",omitempty"`
847 | PolitenessMode int `json:",omitempty"`
848 | ReplyEmail string `json:",omitempty"`
849 | SegmentationID int64 `json:",omitempty"`
850 | SegmentationALT string `json:",omitempty"`
851 | Sender string
852 | SenderEmail string
853 | SenderName string `json:",omitempty"`
854 | Status string `json:",omitempty"`
855 | Subject string
856 | TemplateID int64 `json:",omitempty"`
857 | TestAddress string `json:",omitempty"`
858 | Title string `json:",omitempty"`
859 | URL string `json:"Url,omitempty"`
860 | }
861 |
862 | // NewsletterDetailcontent: An action to upload the content of the newsletter
863 | type NewsletterDetailcontent struct {
864 | TextPart string `json:"Text-part,omitempty"`
865 | HtmlPart string `json:"Html-part,omitempty"`
866 | }
867 |
868 | // NewsletterSchedule: An action to schedule a newsletters.
869 | type NewsLetterSchedule struct {
870 | Date *RFC3339DateTime
871 | }
872 |
873 | // NewsletterTest: An action to test a newsletter.
874 | type NewsletterTest struct {
875 | Recipients []Recipient
876 | }
877 |
878 | // Newslettertemplate: Manages a Newsletter Template Properties.
879 | type Newslettertemplate struct {
880 | CategoryID int64 `json:",omitempty"`
881 | CreatedAt *RFC3339DateTime `json:",omitempty"`
882 | Footer string `json:",omitempty"`
883 | FooterAddress string `json:",omitempty"`
884 | FooterWYSIWYGType int `json:",omitempty"`
885 | HeaderFilename string `json:",omitempty"`
886 | HeaderLink string `json:",omitempty"`
887 | HeaderText string `json:",omitempty"`
888 | HeaderURL string `json:"HeaderUrl,omitempty"`
889 | ID int64 `mailjet:"read_only"`
890 | Locale string
891 | Name string `json:",omitempty"`
892 | Permalink string `json:",omitempty"`
893 | PermalinkWYSIWYGType int `json:",omitempty"`
894 | SourceNewsLetterID int64 `json:",omitempty"`
895 | Status string `json:",omitempty"`
896 | }
897 |
898 | // Newslettertemplatecategory: Manage categories for your newsletters.
899 | // Allows you to group newsletters by category.
900 | type Newslettertemplatecategory struct {
901 | Description string
902 | ID int64 `mailjet:"read_only"`
903 | Locale string
904 | ParentCategoryID int64
905 | Value string
906 | }
907 |
908 | // Openinformation: Retrieve informations about messages opened at least once by their recipients.
909 | type Openinformation struct {
910 | ArrivedAt *RFC3339DateTime `mailjet:"read_only"`
911 | CampaignID int64 `mailjet:"read_only"`
912 | CampaignALT string `mailjet:"read_only"`
913 | ContactID int64 `mailjet:"read_only"`
914 | ContactALT string `mailjet:"read_only"`
915 | ID int64 `mailjet:"read_only"`
916 | MessageID int64 `mailjet:"read_only"`
917 | OpenedAt *RFC3339DateTime `mailjet:"read_only"`
918 | UserAgent string `mailjet:"read_only"`
919 | UserAgentFull string `mailjet:"read_only"`
920 | }
921 |
922 | // Openstatistics: Retrieve statistics on e-mails opened at least once by their recipients.
923 | type Openstatistics struct {
924 | OpenedCount int64 `mailjet:"read_only"`
925 | OpenedDelay float64 `mailjet:"read_only"`
926 | ProcessedCount int64 `mailjet:"read_only"`
927 | }
928 |
929 | // Parseroute: ParseRoute description
930 | type Parseroute struct {
931 | APIKeyID int64 `json:",omitempty"`
932 | APIKeyALT string `json:",omitempty"`
933 | Email string `json:",omitempty"`
934 | ID int64 `mailjet:"read_only"`
935 | URL string `json:"Url"`
936 | }
937 |
938 | // Preferences: User preferences in key=value format.
939 | type Preferences struct {
940 | ID int64 `mailjet:"read_only"`
941 | Key string
942 | UserID int64 `json:",omitempty"`
943 | UserALT string `json:",omitempty"`
944 | Value string `json:",omitempty"`
945 | }
946 |
947 | // Preset: The preset object contains global and user defined presets (styles) independent from templates or newsletters.
948 | // Access is similar to template and depends on OwnerType, Owner. No versioning is done. Presets are never referenced by their ID.
949 | // The preset value is copied into the template or newsletter.
950 | type Preset struct {
951 | Author string `json:",omitempty"`
952 | Copyright string `json:",omitempty"`
953 | Description string `json:",omitempty"`
954 | ID int64 `mailjet:"read_only"`
955 | Name string `json:",omitempty"`
956 | OwnerID int64 `json:",omitempty"`
957 | OwnerType string `json:",omitempty"`
958 | Preset string `json:",omitempty"`
959 | }
960 |
961 | // Sender: Manage an email sender for a single API key.
962 | // An e-mail address or a complete domain (*) has to be registered and validated before being used to send e-mails.
963 | // In order to manage a sender available across multiple API keys, see the related MetaSender resource.
964 | type Sender struct {
965 | CreatedAt *RFC3339DateTime `mailjet:"read_only"`
966 | DNS string `mailjet:"read_only"` // deprecated
967 | DNSID int64 `mailjet:"read_only"`
968 | Email string
969 | EmailType string `json:",omitempty"`
970 | Filename string `mailjet:"read_only"`
971 | ID int64 `mailjet:"read_only"`
972 | IsDefaultSender bool `json:",omitempty"`
973 | Name string `json:",omitempty"`
974 | Status string `mailjet:"read_only"`
975 | }
976 |
977 | // Senderstatistics: API Key sender email address message/open/click statistical information.
978 | type Senderstatistics struct {
979 | BlockedCount int64 `mailjet:"read_only"`
980 | BouncedCount int64 `mailjet:"read_only"`
981 | ClickedCount int64 `mailjet:"read_only"`
982 | DeliveredCount int64 `mailjet:"read_only"`
983 | LastActivityAt *RFC3339DateTime `mailjet:"read_only"`
984 | OpenedCount int64 `mailjet:"read_only"`
985 | ProcessedCount int64 `mailjet:"read_only"`
986 | QueuedCount int64 `mailjet:"read_only"`
987 | SenderID int64 `mailjet:"read_only"`
988 | SenderALT string `mailjet:"read_only"`
989 | SpamComplaintCount int64 `mailjet:"read_only"`
990 | UnsubscribedCount int64 `mailjet:"read_only"`
991 | }
992 |
993 | // SenderValidate: validation result for a sender or domain
994 | type SenderValidate struct {
995 | Errors map[string]string `mailjet:"read_only"`
996 | ValidationMethod string `mailjet:"read_only"`
997 | GlobalError string `mailjet:"read_only"`
998 | }
999 |
1000 | // Template: template description
1001 | type Template struct {
1002 | Author string `json:",omitempty"`
1003 | Categories []string `json:",omitempty"`
1004 | Copyright string `json:",omitempty"`
1005 | Description string `json:",omitempty"`
1006 | EditMode int `json:",omitempty"`
1007 | ID int64 `mailjet:"read_only"`
1008 | IsStarred bool `json:",omitempty"`
1009 | Name string `json:",omitempty"`
1010 | OwnerId int `mailjet:"read_only"`
1011 | OwnerType string `json:",omitempty"`
1012 | Presets string `json:",omitempty"`
1013 | Previews []int64 `mailjet:"read_only"`
1014 | Purposes []string `json:",omitempty"`
1015 | }
1016 |
1017 | // TemplateDetailcontent: GET, POST are supported to read, create, modify and delete the content of a template
1018 | type TemplateDetailcontent struct {
1019 | TextPart string `json:"Text-part,omitempty"`
1020 | HtmlPart string `json:"Html-part,omitempty"`
1021 | MJMLContent MJMLContent `json:",omitempty"`
1022 | Headers interface{} `json:",omitempty"`
1023 | }
1024 |
1025 | // MJMLContent: Structure of Passport template.
1026 | type MJMLContent struct {
1027 | tagName string
1028 | attributes map[string]interface{}
1029 | id string
1030 | }
1031 |
1032 | // Toplinkclicked: Top links clicked historgram.
1033 | type Toplinkclicked struct {
1034 | ClickedCount int64 `mailjet:"read_only"`
1035 | ID int64 `mailjet:"read_only"`
1036 | LinkID int64 `json:"LinkId" mailjet:"read_only"`
1037 | URL string `json:"Url" mailjet:"read_only"`
1038 | }
1039 |
1040 | // Trigger: Triggers for outgoing events.
1041 | type Trigger struct {
1042 | AddedTs int64 `json:",omitempty"`
1043 | APIKey int `json:",omitempty"`
1044 | Details string `json:",omitempty"`
1045 | Event string `json:",omitempty"`
1046 | ID int64 `mailjet:"read_only"`
1047 | User int `json:",omitempty"`
1048 | }
1049 |
1050 | // User: User account definition for Mailjet.
1051 | type User struct {
1052 | ACL string `json:",omitempty"`
1053 | CreatedAt *RFC3339DateTime `mailjet:"read_only"`
1054 | Email string `json:",omitempty"`
1055 | ID int64 `mailjet:"read_only"`
1056 | LastIp string
1057 | LastLoginAt *RFC3339DateTime `json:",omitempty"`
1058 | Locale string `json:",omitempty"`
1059 | MaxAllowedAPIKeys int `mailjet:"read_only"`
1060 | Timezone string `json:",omitempty"`
1061 | Username string
1062 | WarnedRatelimitAt *RFC3339DateTime `json:",omitempty"`
1063 | }
1064 |
1065 | // Useragentstatistics: View statistics on User Agents.
1066 | // See total counts or filter per Campaign or Contacts List.
1067 | // API Key message Open/Click statistical data grouped per user agent (browser).
1068 | type Useragentstatistics struct {
1069 | Count int64 `mailjet:"read_only"`
1070 | DistinctCount int64 `mailjet:"read_only"`
1071 | Platform string `mailjet:"read_only"`
1072 | UserAgent string `mailjet:"read_only"`
1073 | }
1074 |
1075 | // Widget: Manage settings for Widgets.
1076 | // Widgets are small registration forms that you may include on your website to ease the process of subscribing to a Contacts List.
1077 | // Mailjet widget definitions.
1078 | type Widget struct {
1079 | CreatedAt int64 `json:",omitempty"`
1080 | FromID int64 `json:",omitempty"`
1081 | FromALT string `json:",omitempty"`
1082 | ID int64 `mailjet:"read_only"`
1083 | IsActive bool `json:",omitempty"`
1084 | ListID int64 `json:",omitempty"`
1085 | ListALT string `json:",omitempty"`
1086 | Locale string
1087 | Name string `json:",omitempty"`
1088 | Replyto string `json:",omitempty"`
1089 | Sendername string `json:",omitempty"`
1090 | Subject string `json:",omitempty"`
1091 | Template *MessageTemplate `json:",omitempty"`
1092 | }
1093 |
1094 | // Widgetcustomvalue: Specifics settings for a given Mailjet Widget. See Widget.Mailjet widget settings.
1095 | type Widgetcustomvalue struct {
1096 | APIKeyID int64 `json:",omitempty"`
1097 | APIKeyALT string `json:",omitempty"`
1098 | Display bool `json:",omitempty"`
1099 | ID int64 `mailjet:"read_only"`
1100 | Name string
1101 | Value string `json:",omitempty"`
1102 | WidgetID int64
1103 | }
1104 |
1105 | type AXTestMode string
1106 |
1107 | const (
1108 | AXTestAutomatic = AXTestMode("automatic")
1109 | AXTestManual = "manual"
1110 | )
1111 |
1112 | type RunLevel string
1113 |
1114 | const (
1115 | RunNormal = RunLevel("Normal")
1116 | RunSoftlock = "Softlock"
1117 | RunHardlock = "Hardlock"
1118 | )
1119 |
1120 | type WinnerMethod string
1121 |
1122 | const (
1123 | WinnerOpenRate = WinnerMethod("OpenRate")
1124 | WinnerClickRate = "ClickRate"
1125 | WinnerSpamRate = "SpamRate"
1126 | WinnerUnsubRate = "UnsubRate"
1127 | WinnerMJScore = "MJScore"
1128 | )
1129 |
1130 | type BaseData struct {
1131 | DataAsString string
1132 | DataType string
1133 | }
1134 |
1135 | type SubAccount struct {
1136 | IsActive bool
1137 | CreatedAt *RFC3339DateTime
1138 | Email string
1139 | FirstIP string `json:"FirstIp"`
1140 | Firstname string
1141 | LastIP string `json:"LastIp"`
1142 | LastLoginAt *RFC3339DateTime
1143 | Lastname string
1144 | NewPasswordKey string
1145 | Password string
1146 | }
1147 |
1148 | type Destination struct {
1149 | Domain string
1150 | IsNeverBlocked bool
1151 | }
1152 |
1153 | type SpamAssassinRule struct {
1154 | HitCount int64
1155 | Name string
1156 | Score float64
1157 | }
1158 |
1159 | type MessageTemplate struct {
1160 | Category string
1161 | ContentHtml string
1162 | ContentText string
1163 | DefaultType string
1164 | Footer string
1165 | FromAddr string
1166 | FromName string
1167 | Name string
1168 | Locale string
1169 | ReplyTo string
1170 | Subject string
1171 | Subtitle string
1172 | Template string
1173 | Title string
1174 | }
1175 |
1176 | type KeyValueList []map[string]string
1177 |
1178 | type JSONObject interface{}
1179 |
1180 | type ContactsListAction struct {
1181 | ListID int64
1182 | Action string
1183 | }
1184 |
1185 | type AddContactAction struct {
1186 | Email string
1187 | Name string
1188 | IsExcludedFromCampaigns *bool `json:",omitempty"`
1189 | Properties JSONObject
1190 | }
1191 |
1192 | type Recipient struct {
1193 | Email string
1194 | Name string
1195 | }
1196 |
1197 | type RFC3339DateTime struct {
1198 | time.Time
1199 | }
1200 |
1201 | func (dt *RFC3339DateTime) UnmarshalJSON(b []byte) (err error) {
1202 | b = bytes.Trim(b, `" `)
1203 | if b == nil {
1204 | return nil
1205 | }
1206 |
1207 | dt.Time, err = time.Parse(time.RFC3339, string(b))
1208 |
1209 | return err
1210 | }
1211 |
1212 | func (dt *RFC3339DateTime) MarshalJSON() ([]byte, error) {
1213 | return []byte(dt.Format(`"` + time.RFC3339 + `"`)), nil
1214 | }
1215 |
--------------------------------------------------------------------------------
/resources/time_test.go:
--------------------------------------------------------------------------------
1 | package resources
2 |
3 | import (
4 | "bytes"
5 | "testing"
6 | "time"
7 | )
8 |
9 | func TestMarshalJSON(t *testing.T) {
10 | dateTime, _ := time.Parse(time.RFC3339, "2016-10-14T12:42:05Z")
11 | dt := RFC3339DateTime{dateTime}
12 |
13 | want := []byte(`"2016-10-14T12:42:05Z"`)
14 | got, err := dt.MarshalJSON()
15 |
16 | if err != nil {
17 | t.Error("unexpected error:", err)
18 | }
19 |
20 | if !bytes.Equal(got, want) {
21 | t.Errorf("expected %v", want)
22 | t.Errorf(" got %v", got)
23 | }
24 | }
25 |
26 | func TestUnmarshalJSON(t *testing.T) {
27 | data := []byte(`"2016-10-14T12:42:05Z"`)
28 | var dt RFC3339DateTime
29 |
30 | err := dt.UnmarshalJSON(data)
31 | if err != nil {
32 | t.Error("unexpected error:", err)
33 | }
34 |
35 | if dt.UnixNano() != 1476448925000000000 {
36 | t.Errorf("expected: %v", 1476448925000000000)
37 | t.Errorf(" got: %v", dt.UnixNano())
38 | }
39 | }
40 |
41 | func TestUnmarshalEmptyJSON(t *testing.T) {
42 | var data []byte
43 | var dt RFC3339DateTime
44 |
45 | err := dt.UnmarshalJSON(data)
46 | if err != nil {
47 | t.Error("unexpected error:", err)
48 | }
49 |
50 | data = []byte{34, 34}
51 | err = dt.UnmarshalJSON(data)
52 | if err != nil {
53 | t.Error("unexpected error:", err)
54 | }
55 | }
56 |
57 | func TestUnmarshalBrokenJSON(t *testing.T) {
58 | data := []byte{1, 1}
59 | var dt RFC3339DateTime
60 |
61 | err := dt.UnmarshalJSON(data)
62 | if err == nil {
63 | t.Error("error expected")
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/smtp_client.go:
--------------------------------------------------------------------------------
1 | package mailjet
2 |
3 | import (
4 | "fmt"
5 | "net/smtp"
6 | )
7 |
8 | // SMTPClient is the wrapper for smtp
9 | type SMTPClient struct {
10 | host string
11 | auth smtp.Auth
12 | }
13 |
14 | // Hostname and port for the SMTP client.
15 | const (
16 | HostSMTP = "in-v3.mailjet.com"
17 | PortSMTP = 587
18 | )
19 |
20 | // NewSMTPClient returns a new smtp client wrapper
21 | func NewSMTPClient(apiKeyPublic, apiKeyPrivate string) *SMTPClient {
22 | auth := smtp.PlainAuth(
23 | "",
24 | apiKeyPublic,
25 | apiKeyPrivate,
26 | HostSMTP,
27 | )
28 | return &SMTPClient{
29 | host: fmt.Sprintf("%s:%d", HostSMTP, PortSMTP),
30 | auth: auth,
31 | }
32 | }
33 |
34 | // SendMail wraps smtp.SendMail
35 | func (s SMTPClient) SendMail(from string, to []string, msg []byte) error {
36 | return smtp.SendMail(s.host, s.auth, from, to, msg)
37 | }
38 |
--------------------------------------------------------------------------------
/smtp_client_decl.go:
--------------------------------------------------------------------------------
1 | package mailjet
2 |
3 | // SMTPClientInterface def
4 | type SMTPClientInterface interface {
5 | SendMail(from string, to []string, msg []byte) error
6 | }
7 |
--------------------------------------------------------------------------------
/smtp_client_mock.go:
--------------------------------------------------------------------------------
1 | package mailjet
2 |
3 | import (
4 | "errors"
5 | )
6 |
7 | // SMTPClientMock def
8 | type SMTPClientMock struct {
9 | valid bool
10 | }
11 |
12 | // NewSMTPClientMock returns a new smtp client mock
13 | func NewSMTPClientMock(valid bool) *SMTPClientMock {
14 | return &SMTPClientMock{
15 | valid: valid,
16 | }
17 | }
18 |
19 | // SendMail wraps smtp.SendMail
20 | func (s SMTPClientMock) SendMail(from string, to []string, msg []byte) error {
21 | if s.valid {
22 | return nil
23 | }
24 | return errors.New("smtp send error")
25 | }
26 |
--------------------------------------------------------------------------------
/tests/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "math/rand"
7 | "net/textproto"
8 | "os"
9 | "sync"
10 | "time"
11 |
12 | "github.com/mailjet/mailjet-apiv3-go/v4"
13 | "github.com/mailjet/mailjet-apiv3-go/v4/resources"
14 | )
15 |
16 | var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
17 |
18 | func randSeq(n int) string {
19 | rand.Seed(time.Now().UnixNano())
20 | b := make([]rune, n)
21 | for i := range b {
22 | //nolint:gosec // G404 crypto random is not required here
23 | b[i] = letters[rand.Intn(len(letters))]
24 | }
25 | return string(b)
26 | }
27 |
28 | func testNewMailjetClient() {
29 | ak := os.Getenv("MJ_APIKEY_PUBLIC")
30 | sk := os.Getenv("MJ_APIKEY_PRIVATE")
31 | m := mailjet.NewMailjetClient(ak, sk)
32 |
33 | if ak != m.APIKeyPublic() {
34 | log.Fatal("Wrong public key:", m.APIKeyPublic())
35 | }
36 |
37 | if sk != m.APIKeyPrivate() {
38 | log.Fatal("Wrong secret key:", m.APIKeyPrivate())
39 | }
40 | }
41 |
42 | func testList() {
43 | m := mailjet.NewMailjetClient(os.Getenv("MJ_APIKEY_PUBLIC"), os.Getenv("MJ_APIKEY_PRIVATE"))
44 |
45 | var data []resources.Sender
46 | count, _, err := m.List("sender", &data)
47 | if err != nil {
48 | log.Fatal("Unexpected error:", err)
49 | }
50 | if count < 1 {
51 | log.Fatal("At least one sender expected !")
52 | }
53 | }
54 |
55 | func testGet() {
56 | m := mailjet.NewMailjetClient(os.Getenv("MJ_APIKEY_PUBLIC"), os.Getenv("MJ_APIKEY_PRIVATE"))
57 |
58 | var data []resources.User
59 | resource := "user"
60 | count, _, err := m.List(resource, &data)
61 | if err != nil {
62 | log.Fatal("Unexpected error:", err)
63 | }
64 | if count < 1 {
65 | log.Fatal("At least one user expected !")
66 | }
67 | if data == nil {
68 | log.Fatal("Empty result")
69 | }
70 |
71 | mr := &mailjet.Request{Resource: resource, ID: data[0].ID}
72 | data = make([]resources.User, 0)
73 | err = m.Get(mr, &data)
74 | if err != nil {
75 | log.Fatal("Unexpected error:", err)
76 | }
77 | fmt.Printf("Data: %+v\n", data[0])
78 | }
79 |
80 | func testPost() {
81 | m := mailjet.NewMailjetClient(os.Getenv("MJ_APIKEY_PUBLIC"), os.Getenv("MJ_APIKEY_PRIVATE"))
82 |
83 | var data []resources.Contact
84 | rstr := randSeq(10)
85 | fmt.Printf("Create new contact: \"%s@mailjet.com\"\n", rstr)
86 | fmr := &mailjet.FullRequest{
87 | Info: &mailjet.Request{Resource: "contact"},
88 | Payload: &resources.Contact{Name: rstr, Email: rstr + "@mailjet.com"},
89 | }
90 | err := m.Post(fmr, &data)
91 | if err != nil {
92 | log.Fatal("Unexpected error:", err)
93 | }
94 | if data == nil {
95 | log.Fatal("Empty result")
96 | }
97 | fmt.Printf("Data: %+v\n", data[0])
98 | }
99 |
100 | func testPut() {
101 | m := mailjet.NewMailjetClient(os.Getenv("MJ_APIKEY_PUBLIC"), os.Getenv("MJ_APIKEY_PRIVATE"))
102 |
103 | var data []resources.Contactslist
104 | resource := "contactslist"
105 | count, _, err := m.List(resource, &data)
106 | if err != nil {
107 | log.Fatal("Unexpected error:", err)
108 | }
109 | if count < 1 {
110 | log.Fatal("At least one contact list expected on test account!")
111 | }
112 | if data == nil {
113 | log.Fatal("Empty result")
114 | }
115 |
116 | rstr := randSeq(10)
117 | fmt.Printf("Update name of the contact list: %s -> %s\n", data[0].Name, rstr)
118 | data[0].Name = randSeq(10)
119 | fmr := &mailjet.FullRequest{
120 | Info: &mailjet.Request{Resource: resource, AltID: data[0].Address},
121 | Payload: data[0],
122 | }
123 | err = m.Put(fmr, []string{"Name"})
124 | if err != nil {
125 | log.Fatal("Unexpected error:", err)
126 | }
127 | }
128 |
129 | func testDelete() {
130 | m := mailjet.NewMailjetClient(os.Getenv("MJ_APIKEY_PUBLIC"), os.Getenv("MJ_APIKEY_PRIVATE"))
131 |
132 | var data []resources.Listrecipient
133 | resource := "listrecipient"
134 | count, _, err := m.List(resource, &data)
135 | if err != nil {
136 | log.Fatal("Unexpected error:", err)
137 | }
138 | if count < 1 {
139 | return
140 | }
141 | if data == nil {
142 | log.Fatal("Empty result")
143 | }
144 |
145 | mr := &mailjet.Request{
146 | ID: data[0].ID,
147 | Resource: resource,
148 | }
149 | err = m.Delete(mr)
150 | if err != nil {
151 | fmt.Println(err)
152 | }
153 | }
154 |
155 | func testSendMail() {
156 | m := mailjet.NewMailjetClient(os.Getenv("MJ_APIKEY_PUBLIC"), os.Getenv("MJ_APIKEY_PRIVATE"))
157 |
158 | var data []resources.Sender
159 | count, _, err := m.List("sender", &data)
160 | if err != nil {
161 | log.Fatal("Unexpected error:", err)
162 | }
163 | if count < 1 || data == nil {
164 | log.Fatal("At least one sender expected in the test account!")
165 | }
166 |
167 | param := &mailjet.InfoSendMail{
168 | FromEmail: data[0].Email,
169 | FromName: data[0].Name,
170 | Recipients: []mailjet.Recipient{
171 | {
172 | Email: data[0].Email,
173 | },
174 | },
175 | Subject: "Send API testing",
176 | TextPart: "SendMail is working !",
177 | }
178 | res, err := m.SendMail(param)
179 | if err != nil {
180 | log.Fatal("Unexpected error:", err)
181 | }
182 | fmt.Printf("Data: %+v\n", res)
183 | }
184 |
185 | func testSendMailSMTP() {
186 | mj := mailjet.NewMailjetClient(
187 | os.Getenv("MJ_APIKEY_PUBLIC"),
188 | os.Getenv("MJ_APIKEY_PRIVATE"))
189 |
190 | var data []resources.Sender
191 | count, _, err := mj.List("sender", &data)
192 | if err != nil {
193 | log.Fatal("Unexpected error:", err)
194 | }
195 | if count < 1 || data == nil {
196 | log.Fatal("At least one sender expected in the test account!")
197 | }
198 |
199 | email := data[0].Email
200 |
201 | header := make(textproto.MIMEHeader)
202 | header.Add("From", email)
203 | header.Add("To", email)
204 | header.Add("Subject", "SMTP testing")
205 | header.Add("X-Mailjet-Campaign", "test")
206 | content := []byte("SendMailSmtp is working !")
207 | info := &mailjet.InfoSMTP{
208 | From: email,
209 | Recipients: header["To"],
210 | Header: header,
211 | Content: content,
212 | }
213 | err = mj.SendMailSMTP(info)
214 | if err != nil {
215 | log.Fatal("Unexpected error:", err)
216 | }
217 | }
218 |
219 | func testDataRace() {
220 | m := mailjet.NewMailjetClient(os.Getenv("MJ_APIKEY_PUBLIC"), os.Getenv("MJ_APIKEY_PRIVATE"))
221 |
222 | var wg sync.WaitGroup
223 | wg.Add(5)
224 |
225 | for i := 0; i < 5; i++ {
226 | go func() {
227 | var data []resources.Sender
228 | count, _, err := m.List("sender", &data)
229 | if err != nil {
230 | log.Fatal("Unexpected error:", err)
231 | }
232 | if count < 1 || data == nil {
233 | log.Fatal("At least one sender expected in the test account!")
234 | }
235 | wg.Done()
236 | }()
237 | }
238 | wg.Wait()
239 | }
240 |
241 | func main() {
242 | testNewMailjetClient()
243 | testList()
244 | testGet()
245 | testPost()
246 | testPut()
247 | testDelete()
248 | testSendMail()
249 | testSendMailSMTP()
250 | testDataRace()
251 | }
252 |
--------------------------------------------------------------------------------