Compare commits

...

67 Commits

Author SHA1 Message Date
2c88f50625 2.0.2 2025-10-13 20:20:24 +00:00
cc62c24e7b fix(mailgun): Normalize package scope and modernize Mailgun client: rename package to @apiclient.xyz/mailgun, update dependencies, refactor HTTP handling, fix types, update TS config and CI, refresh docs and tests 2025-10-13 20:20:24 +00:00
7a66f97700 2.0.1 2022-08-07 19:27:43 +02:00
9905785ef3 fix(core): update 2022-08-07 19:27:42 +02:00
63f515b801 2.0.0 2022-08-07 19:07:15 +02:00
c85c8d64fd BREAKING CHANGE(core): switch to esm 2022-08-07 19:07:15 +02:00
7189d465db 1.0.32 2022-08-07 19:07:02 +02:00
9350df0565 fix(core): update 2022-08-07 19:07:01 +02:00
030a205085 1.0.31 2022-08-07 16:31:56 +02:00
f7a7133324 fix(core): update 2022-08-07 16:31:56 +02:00
31d8baa99c 1.0.30 2020-08-15 13:07:31 +00:00
3322398901 fix(core): update 2020-08-15 13:07:31 +00:00
ece6b6feda 1.0.29 2020-08-13 01:37:26 +00:00
108f483823 fix(core): update 2020-08-13 01:37:26 +00:00
0412cecfe5 1.0.28 2020-08-13 00:32:06 +00:00
4cb32e8460 fix(core): update 2020-08-13 00:32:05 +00:00
3abc973d88 1.0.27 2020-08-11 14:47:26 +00:00
b08b9abb38 fix(core): update 2020-08-11 14:47:25 +00:00
13e064207f 1.0.26 2020-06-18 22:00:00 +00:00
5d4b0eff68 fix(core): update 2020-06-18 22:00:00 +00:00
29a7472b3c 1.0.25 2020-04-26 23:24:03 +00:00
5490be1033 fix(core): update 2020-04-26 23:24:03 +00:00
6ee7c6af76 1.0.24 2020-01-23 17:31:11 +00:00
aa1750524b fix(core): update 2020-01-23 17:31:10 +00:00
da7c57165e 1.0.23 2020-01-23 16:55:21 +00:00
708c05d1e3 fix(core): update 2020-01-23 16:55:20 +00:00
58b7f602bd 1.0.22 2020-01-21 10:07:07 +00:00
23507b83fc fix(core): update 2020-01-21 10:07:07 +00:00
c160cfd94e 1.0.21 2020-01-21 10:04:29 +00:00
920399a495 fix(core): update 2020-01-21 10:04:28 +00:00
168cb9d5d7 1.0.20 2020-01-13 16:04:25 +00:00
f61e119912 fix(core): update 2020-01-13 16:04:25 +00:00
f5c3fc6ee6 1.0.19 2020-01-13 14:34:14 +00:00
6643b9c3dd fix(core): update 2020-01-13 14:34:13 +00:00
157bf6a893 fix(interfaces): export interfaces 2020-01-13 14:06:34 +00:00
5d5e2076d8 1.0.17 2020-01-13 08:09:37 +00:00
815b455dbb fix(core): update 2020-01-13 08:09:37 +00:00
9d5907a7a7 1.0.16 2020-01-11 19:02:00 +00:00
0212d476c2 fix(core): update 2020-01-11 19:01:59 +00:00
2b8aadc816 1.0.15 2020-01-11 11:15:53 +00:00
1149ee6759 fix(core): update 2020-01-11 11:15:52 +00:00
fa012409ff 1.0.14 2020-01-11 11:15:12 +00:00
40bd96bf41 fix(core): update 2020-01-11 11:15:11 +00:00
f3591bdc67 1.0.13 2020-01-11 11:13:41 +00:00
ece9508161 fix(core): update 2020-01-11 11:13:41 +00:00
761d7e78de 1.0.12 2020-01-11 11:11:49 +00:00
0c44cf09b4 fix(core): update 2020-01-11 11:11:48 +00:00
aee7c80e26 1.0.11 2019-10-28 16:15:16 +01:00
c60702fb49 fix(core): update 2019-10-28 16:15:16 +01:00
354c38a429 1.0.10 2019-10-28 16:10:20 +01:00
f477cad6e9 fix(core): update 2019-10-28 16:10:20 +01:00
631aaadb43 1.0.9 2019-10-28 16:07:15 +01:00
a831cf7e9a fix(core): update 2019-10-28 16:07:14 +01:00
e6125c9a13 1.0.8 2019-10-28 15:57:11 +01:00
a203965e29 1.0.7 2019-10-28 15:55:04 +01:00
4305b53e35 fix(core): update 2019-10-28 15:55:04 +01:00
b7c657a930 1.0.6 2019-10-27 22:53:22 +01:00
0a758cdb60 fix(core): update 2019-10-27 22:53:21 +01:00
b97740094b 1.0.5 2019-10-26 23:55:50 +02:00
8947c6e8de fix(core): update 2019-10-26 23:55:50 +02:00
46e2f54974 1.0.4 2019-10-23 20:00:05 +02:00
6beb085488 fix(core): update 2019-10-23 20:00:04 +02:00
a32b31b86d 1.0.3 2019-10-23 19:27:55 +02:00
1371bd0a55 1.0.2 2019-10-23 19:27:43 +02:00
fa23bab37b fix(core): update 2019-10-23 19:27:42 +02:00
2e77b3672a 1.0.2 2017-02-08 23:28:10 +01:00
d734ac821e fix npmts error 2017-02-08 23:27:58 +01:00
25 changed files with 11429 additions and 40 deletions

View File

@@ -0,0 +1,66 @@
name: Default (not tags)
on:
push:
tags-ignore:
- '**'
env:
IMAGE: code.foss.global/host.today/ht-docker-node:npmci
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
NPMCI_URL_CLOUDLY: ${{secrets.NPMCI_URL_CLOUDLY}}
jobs:
security:
runs-on: ubuntu-latest
continue-on-error: true
container:
image: ${{ env.IMAGE }}
steps:
- uses: actions/checkout@v3
- name: Install pnpm and npmci
run: |
pnpm install -g pnpm
pnpm install -g @ship.zone/npmci
- name: Run npm prepare
run: npmci npm prepare
- name: Audit production dependencies
run: |
npmci command npm config set registry https://registry.npmjs.org
npmci command pnpm audit --audit-level=high --prod
continue-on-error: true
- name: Audit development dependencies
run: |
npmci command npm config set registry https://registry.npmjs.org
npmci command pnpm audit --audit-level=high --dev
continue-on-error: true
test:
if: ${{ always() }}
needs: security
runs-on: ubuntu-latest
container:
image: ${{ env.IMAGE }}
steps:
- uses: actions/checkout@v3
- name: Test stable
run: |
npmci node install stable
npmci npm install
npmci npm test
- name: Test build
run: |
npmci node install stable
npmci npm install
npmci npm build

View File

@@ -0,0 +1,124 @@
name: Default (tags)
on:
push:
tags:
- '*'
env:
IMAGE: code.foss.global/host.today/ht-docker-node:npmci
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
NPMCI_URL_CLOUDLY: ${{secrets.NPMCI_URL_CLOUDLY}}
jobs:
security:
runs-on: ubuntu-latest
continue-on-error: true
container:
image: ${{ env.IMAGE }}
steps:
- uses: actions/checkout@v3
- name: Prepare
run: |
pnpm install -g pnpm
pnpm install -g @ship.zone/npmci
npmci npm prepare
- name: Audit production dependencies
run: |
npmci command npm config set registry https://registry.npmjs.org
npmci command pnpm audit --audit-level=high --prod
continue-on-error: true
- name: Audit development dependencies
run: |
npmci command npm config set registry https://registry.npmjs.org
npmci command pnpm audit --audit-level=high --dev
continue-on-error: true
test:
if: ${{ always() }}
needs: security
runs-on: ubuntu-latest
container:
image: ${{ env.IMAGE }}
steps:
- uses: actions/checkout@v3
- name: Prepare
run: |
pnpm install -g pnpm
pnpm install -g @ship.zone/npmci
npmci npm prepare
- name: Test stable
run: |
npmci node install stable
npmci npm install
npmci npm test
- name: Test build
run: |
npmci node install stable
npmci npm install
npmci npm build
release:
needs: test
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
container:
image: ${{ env.IMAGE }}
steps:
- uses: actions/checkout@v3
- name: Prepare
run: |
pnpm install -g pnpm
pnpm install -g @ship.zone/npmci
npmci npm prepare
- name: Release
run: |
npmci node install stable
npmci npm publish
metadata:
needs: test
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
container:
image: ${{ env.IMAGE }}
continue-on-error: true
steps:
- uses: actions/checkout@v3
- name: Prepare
run: |
pnpm install -g pnpm
pnpm install -g @ship.zone/npmci
npmci npm prepare
- name: Code quality
run: |
npmci command npm install -g typescript
npmci npm install
- name: Trigger
run: npmci trigger
- name: Build docs and upload artifacts
run: |
npmci node install stable
npmci npm install
pnpm install -g @git.zone/tsdoc
npmci command tsdoc
continue-on-error: true

23
.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
.nogit/
# artifacts
coverage/
public/
# installs
node_modules/
# caches
.yarn/
.cache/
.rpt2_cache
# builds
dist/
dist_*/
# AI
.claude/
.serena/
#------# custom

View File

@@ -1,59 +1,128 @@
image: hosttoday/ht-docker-node:npmts # gitzone ci_default
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
cache:
paths:
- .npmci_cache/
key: '$CI_BUILD_STAGE'
stages: stages:
- security
- test - test
- release - release
- trigger - metadata
- pages
testLEGACY: before_script:
stage: test - npm install -g @shipzone/npmci
# ====================
# security stage
# ====================
auditProductionDependencies:
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
stage: security
script: script:
- npmci test legacy - npmci npm prepare
- npmci command npm install --production --ignore-scripts
- npmci command npm config set registry https://registry.npmjs.org
- npmci command npm audit --audit-level=high --only=prod --production
tags: tags:
- docker - docker
allow_failure: true allow_failure: true
testLTS: auditDevDependencies:
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
stage: security
script:
- npmci npm prepare
- npmci command npm install --ignore-scripts
- npmci command npm config set registry https://registry.npmjs.org
- npmci command npm audit --audit-level=high --only=dev
tags:
- docker
allow_failure: true
# ====================
# test stage
# ====================
testStable:
stage: test stage: test
script: script:
- npmci test lts - npmci npm prepare
- npmci node install stable
- npmci npm install
- npmci npm test
coverage: /\d+.?\d+?\%\s*coverage/
tags: tags:
- docker - docker
testSTABLE: testBuild:
stage: test stage: test
script: script:
- npmci test stable - npmci npm prepare
- npmci node install stable
- npmci npm install
- npmci command npm run build
coverage: /\d+.?\d+?\%\s*coverage/
tags: tags:
- docker - docker
release: release:
stage: release stage: release
script: script:
- npmci publish - npmci node install stable
- npmci npm publish
only: only:
- tags - tags
tags: tags:
- lossless
- docker - docker
- notpriv
# ====================
# metadata stage
# ====================
codequality:
stage: metadata
allow_failure: true
only:
- tags
script:
- npmci command npm install -g typescript
- npmci npm prepare
- npmci npm install
tags:
- lossless
- docker
- priv
trigger: trigger:
stage: trigger stage: metadata
script: script:
- npmci trigger - npmci trigger
only: only:
- tags - tags
tags: tags:
- lossless
- docker - docker
- notpriv
pages: pages:
image: hosttoday/ht-docker-node:npmpage stage: metadata
stage: pages
script: script:
- npmci command npmpage --publish gitlab - npmci node install stable
- npmci npm prepare
- npmci npm install
- npmci command npm run buildDocs
tags:
- lossless
- docker
- notpriv
only: only:
- tags - tags
artifacts: artifacts:
expire_in: 1 week expire_in: 1 week
paths: paths:
- public - public
allow_failure: true

11
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "npm test",
"name": "Run npm test",
"request": "launch",
"type": "node-terminal"
}
]
}

26
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,26 @@
{
"json.schemas": [
{
"fileMatch": ["/npmextra.json"],
"schema": {
"type": "object",
"properties": {
"npmci": {
"type": "object",
"description": "settings for npmci"
},
"gitzone": {
"type": "object",
"description": "settings for gitzone",
"properties": {
"projectType": {
"type": "string",
"enum": ["website", "element", "service", "npm", "wcc"]
}
}
}
}
}
}
]
}

55
changelog.md Normal file
View File

@@ -0,0 +1,55 @@
# Changelog
## 2025-10-13 - 2.0.2 - fix(mailgun)
Normalize package scope and modernize Mailgun client: rename package to @apiclient.xyz/mailgun, update dependencies, refactor HTTP handling, fix types, update TS config and CI, refresh docs and tests
- Rename package and metadata to @apiclient.xyz/mailgun (package.json, npmextra.json, readme and commitinfo)
- Update dependency scopes and versions (@push.rocks packages and dev deps)
- Refactor smartrequest usage to SmartRequest fluent API, replace statusCode/body with status and async json() handling
- Adjust request/form types and attachment handling (FormField shape, filename field, SmartFile usage and Buffer conversion)
- Improve TypeScript configuration (NodeNext moduleResolution, emitDecoratorMetadata, verbatimModuleSyntax) and minor TS fixes in code
- Add Gitea CI workflows (.gitea/workflows) and pnpm workspace config
- Refresh README to reflect new package branding and usage examples
- Fix tests to await env var resolution and format calls, and update .gitignore to include AI artifacts
## 2022-08-07 - 2.0.0 - core
Major release with core updates.
- Core updates and fixes.
- Bumped major version to 2.0.0.
## 2022-08-07 - 1.0.32 - core (BREAKING)
Breaking change: module format change.
- BREAKING CHANGE: switched to ECMAScript modules (ESM). Consumers may need to update import/require usage.
## 2022-08-07 - 1.0.31 - core
Patch release with core fixes.
- Various small core fixes and maintenance updates.
## 2022-08-07 - 1.0.3..1.0.30 - patches
Series of patch releases containing routine fixes and updates.
- Multiple patch releases (1.0.3 through 1.0.30) delivering routine core fixes and minor improvements.
- No documented breaking API changes in these patch releases.
## 2022-08-07 - 2.0.1, 1.0.7, 1.0.2 - tags only
Version tags / metadata-only releases.
- These releases only included version tagging/packaging; no functional changes recorded.
## 2020-01-13 - 1.0.17 - interfaces
Export improvement.
- fix(interfaces): export interfaces so they are available to consumers.
## 2017-02-08 - 1.0.1 - packaging
Packaging fix.
- Fixed npmts error and packaging adjustments.
## 2017-02-08 - 1.0.0 - initial
Initial release.
- Initial commit ("mgun").

19
license Normal file
View File

@@ -0,0 +1,19 @@
Copyright (c) 2019 Lossless GmbH (hello@lossless.com)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

18
npmextra.json Normal file
View File

@@ -0,0 +1,18 @@
{
"gitzone": {
"projectType": "npm",
"module": {
"githost": "code.foss.global",
"gitscope": "apiclient.xyz",
"gitrepo": "mailgun",
"description": "an api abstraction package for mailgun",
"npmPackagename": "@apiclient.xyz/mailgun",
"license": "MIT",
"projectDomain": "apiclient.xyz"
}
},
"npmci": {
"npmGlobalTools": [],
"npmAccessLevel": "public"
}
}

View File

@@ -1,22 +1,58 @@
{ {
"name": "mgun", "name": "@apiclient.xyz/mailgun",
"version": "1.0.1", "version": "2.0.2",
"description": "easy mailgun API wrapper", "private": false,
"main": "dist/index.js", "description": "an api abstraction package for mailgun",
"scripts": { "main": "dist_ts/index.js",
"test": "(npmts)" "typings": "dist_ts/index.d.ts",
}, "type": "module",
"repository": {
"type": "git",
"url": "git+ssh://git@gitlab.com/mojoio/mgun.git"
},
"keywords": [
"mailgun"
],
"author": "Lossless GmbH", "author": "Lossless GmbH",
"license": "MIT", "license": "MIT",
"bugs": { "scripts": {
"url": "https://gitlab.com/mojoio/mgun/issues" "test": "(tstest test/)",
"build": "(tsbuild --web --allowimplicitany)",
"format": "(gitzone format)",
"buildDocs": "tsdoc"
}, },
"homepage": "https://gitlab.com/mojoio/mgun#README" "devDependencies": {
"@git.zone/tsbuild": "^2.1.65",
"@git.zone/tstest": "^2.5.0",
"@push.rocks/qenv": "^6.1.3",
"@push.rocks/tapbundle": "^6.0.3",
"@types/node": "^24.7.2"
},
"dependencies": {
"@push.rocks/smartfile": "^11.2.7",
"@push.rocks/smartmail": "^2.1.0",
"@push.rocks/smartrequest": "^4.3.1",
"@push.rocks/smartsmtp": "^3.0.3",
"@push.rocks/smartstring": "^4.0.2"
},
"files": [
"ts/**/*",
"ts_web/**/*",
"dist/**/*",
"dist_*/**/*",
"dist_ts/**/*",
"dist_ts_web/**/*",
"assets/**/*",
"cli.js",
"npmextra.json",
"readme.md"
],
"browserslist": [
"last 1 chrome versions"
],
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34",
"repository": {
"type": "git",
"url": "https://gitlab.com/mojoio/mailgun.git"
},
"bugs": {
"url": "https://gitlab.com/mojoio/mailgun/issues"
},
"homepage": "https://gitlab.com/mojoio/mailgun#readme",
"pnpm": {
"overrides": {}
}
} }

10210
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

2
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,2 @@
onlyBuiltDependencies:
- esbuild

3
qenv.yml Normal file
View File

@@ -0,0 +1,3 @@
required:
- MAILGUN_API_TOKEN
- MAILGUN_SMTP_CREDENTIALS

3
readme.hints.md Normal file
View File

@@ -0,0 +1,3 @@
# Project Readme Hints
This is the initial readme hints file.

351
readme.md Normal file
View File

@@ -0,0 +1,351 @@
# @apiclient.xyz/mailgun
> 🚀 A modern, TypeScript-first API wrapper for Mailgun with smart fallback mechanisms
Send transactional emails through Mailgun's powerful API with an elegant, type-safe interface. This package seamlessly integrates with the [@push.rocks/smartmail](https://www.npmjs.com/package/@push.rocks/smartmail) ecosystem and provides automatic SMTP fallback for edge cases.
## Why @apiclient.xyz/mailgun? 💡
- **🎯 TypeScript Native** - Full type safety with excellent IntelliSense support
- **🔄 Smart Fallback** - Automatically falls back to SMTP when API limitations are hit
- **📎 Attachment Support** - Hassle-free file attachments with built-in handling
- **🌍 Multi-Region** - Support for both EU and US Mailgun regions
- **📬 Message Retrieval** - Fetch stored messages from Mailgun's API
- **🔗 Webhook Integration** - Easy integration with Mailgun webhooks
- **⚡ Modern API** - Clean, fluent interface powered by smartrequest v4
## Installation
```bash
npm install @apiclient.xyz/mailgun
# or
pnpm install @apiclient.xyz/mailgun
# or
yarn add @apiclient.xyz/mailgun
```
## Quick Start 🚀
```typescript
import { MailgunAccount } from '@apiclient.xyz/mailgun';
import { Smartmail } from '@push.rocks/smartmail';
// Initialize your Mailgun account
const mailgun = new MailgunAccount({
apiToken: 'your-mailgun-api-token',
region: 'eu' // or 'us'
});
// Create and send an email
const email = new Smartmail({
from: 'Your App <noreply@yourdomain.com>',
subject: 'Welcome to Our Service! 🎉',
body: '<h1>Hello!</h1><p>Thanks for signing up.</p>'
});
await mailgun.sendSmartMail(email, 'user@example.com');
```
## Core Features
### 📧 Sending Emails
The primary way to send emails is through the `sendSmartMail()` method, which accepts a `Smartmail` object:
```typescript
import { MailgunAccount } from '@apiclient.xyz/mailgun';
import { Smartmail } from '@push.rocks/smartmail';
const mailgun = new MailgunAccount({
apiToken: process.env.MAILGUN_API_TOKEN,
region: 'eu'
});
// Create a rich HTML email
const email = new Smartmail({
from: 'Team <hello@yourdomain.com>',
subject: 'Monthly Newsletter',
body: `
<html>
<body>
<h1>What's New This Month</h1>
<p>Check out our latest updates...</p>
</body>
</html>
`
});
// Send to a recipient
await mailgun.sendSmartMail(email, 'subscriber@example.com');
```
### 📎 Email Attachments
Add attachments seamlessly using the smartmail interface:
```typescript
import { SmartFile } from '@push.rocks/smartfile';
const email = new Smartmail({
from: 'Sales <sales@yourdomain.com>',
subject: 'Your Invoice',
body: '<p>Please find your invoice attached.</p>'
});
// Add attachment from file
const pdfFile = await SmartFile.fromFilePath('./invoice.pdf');
email.addAttachment(pdfFile);
await mailgun.sendSmartMail(email, 'customer@example.com');
```
### 🔄 SMTP Fallback
For edge cases where the Mailgun API has limitations (like empty email bodies), the package automatically falls back to SMTP:
```typescript
// Configure SMTP credentials for fallback
// Format: domain|username|password
await mailgun.addSmtpCredentials(
'yourdomain.com|postmaster@yourdomain.com|your-smtp-password'
);
// This email with empty body will automatically use SMTP
const emptyBodyEmail = new Smartmail({
from: 'System <system@yourdomain.com>',
subject: 'System Notification',
body: ''
});
await mailgun.sendSmartMail(emptyBodyEmail, 'admin@example.com');
// ✅ Automatically sent via SMTP instead of API
```
### 📬 Retrieving Messages
Fetch stored messages from Mailgun's API:
```typescript
// Retrieve by message URL (from Mailgun storage)
const message = await mailgun.retrieveSmartMailFromMessageUrl(
'https://sw.api.mailgun.net/v3/domains/yourdomain.com/messages/...'
);
if (message) {
console.log('Subject:', message.options.subject);
console.log('From:', message.options.from);
console.log('Body:', message.options.body);
// Attachments are automatically fetched
console.log('Attachments:', message.attachments.length);
}
```
### 🔔 Webhook Integration
Process Mailgun webhook notifications:
```typescript
// In your webhook handler
app.post('/webhooks/mailgun', async (req, res) => {
const payload = req.body;
// Retrieve the full message from the webhook payload
const message = await mailgun.retrieveSmartMailFromNotifyPayload(payload);
if (message) {
// Process the received email
console.log('Received email:', message.options.subject);
}
res.status(200).send('OK');
});
```
## API Reference
### MailgunAccount
#### Constructor
```typescript
new MailgunAccount(options: {
apiToken: string; // Your Mailgun API token
region: 'eu' | 'us'; // Mailgun region
})
```
#### Methods
##### `sendSmartMail(smartmail, recipient, data?)`
Send an email through Mailgun.
```typescript
await mailgun.sendSmartMail(
smartmailInstance,
'recipient@example.com',
{ customData: 'optional' } // Optional template data
);
```
##### `addSmtpCredentials(credentials)`
Configure SMTP fallback credentials.
```typescript
await mailgun.addSmtpCredentials('domain|username|password');
```
##### `retrieveSmartMailFromMessageUrl(url)`
Retrieve a stored message by its Mailgun URL.
```typescript
const message = await mailgun.retrieveSmartMailFromMessageUrl(messageUrl);
```
##### `retrieveSmartMailFromNotifyPayload(payload)`
Extract and retrieve a message from a Mailgun webhook payload.
```typescript
const message = await mailgun.retrieveSmartMailFromNotifyPayload(webhookPayload);
```
## Region Support 🌍
Mailgun operates in multiple regions. Choose the appropriate one for your needs:
- **`eu`** - European region (GDPR compliant, data stored in EU)
- **`us`** - US region (data stored in US)
```typescript
// EU region
const mailgunEU = new MailgunAccount({
apiToken: 'your-token',
region: 'eu'
});
// US region
const mailgunUS = new MailgunAccount({
apiToken: 'your-token',
region: 'us'
});
```
## Advanced Usage
### Template Variables with Smartmail
Use template variables in your emails:
```typescript
import { Smartmail } from '@push.rocks/smartmail';
const welcomeEmail = new Smartmail({
from: 'Welcome Team <welcome@yourdomain.com>',
subject: 'Welcome {{userName}}!',
body: '<h1>Hi {{userName}}</h1><p>Your account is ready!</p>'
});
// Pass template data
await mailgun.sendSmartMail(
welcomeEmail,
'newuser@example.com',
{ userName: 'John Doe' }
);
```
### Error Handling
```typescript
try {
await mailgun.sendSmartMail(email, 'user@example.com');
console.log('✅ Email sent successfully');
} catch (error) {
console.error('❌ Failed to send email:', error.message);
}
```
### Batch Sending
Send multiple emails efficiently:
```typescript
const recipients = [
'user1@example.com',
'user2@example.com',
'user3@example.com'
];
const email = new Smartmail({
from: 'Newsletter <news@yourdomain.com>',
subject: 'Weekly Update',
body: '<p>Here is your weekly update...</p>'
});
// Send to all recipients
await Promise.all(
recipients.map(recipient =>
mailgun.sendSmartMail(email, recipient)
)
);
```
## TypeScript Support 📘
This package is written in TypeScript and provides full type definitions out of the box:
```typescript
import {
MailgunAccount,
IMailgunAccountContructorOptions,
IMailgunMessage
} from '@apiclient.xyz/mailgun';
// Full IntelliSense support
const options: IMailgunAccountContructorOptions = {
apiToken: 'token',
region: 'eu'
};
const mailgun = new MailgunAccount(options);
```
## Dependencies
This package leverages the powerful @push.rocks ecosystem:
- **[@push.rocks/smartrequest](https://www.npmjs.com/package/@push.rocks/smartrequest)** - Modern HTTP client
- **[@push.rocks/smartmail](https://www.npmjs.com/package/@push.rocks/smartmail)** - Email composition
- **[@push.rocks/smartsmtp](https://www.npmjs.com/package/@push.rocks/smartsmtp)** - SMTP fallback
- **[@push.rocks/smartfile](https://www.npmjs.com/package/@push.rocks/smartfile)** - File handling
- **[@push.rocks/smartstring](https://www.npmjs.com/package/@push.rocks/smartstring)** - String utilities
## Links & Resources
- **📦 [NPM Package](https://www.npmjs.com/package/@apiclient.xyz/mailgun)**
- **💻 [GitLab Repository](https://gitlab.com/apiclient.xyz/mailgun)**
- **🪞 [GitHub Mirror](https://github.com/apiclient-xyz/mailgun)**
- **📚 [TypeDoc Documentation](https://apiclient.xyz/mailgun/docs/)**
- **🌐 [Mailgun API Docs](https://documentation.mailgun.com/)**
## License and Legal Information
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
### Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
### Company Information
Task Venture Capital GmbH
Registered at District court Bremen HRB 35230 HB, Germany
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

69
test/test.ts Normal file
View File

@@ -0,0 +1,69 @@
import { expect, tap } from '@push.rocks/tapbundle';
import { Qenv } from '@push.rocks/qenv';
import * as smartmail from '@push.rocks/smartmail';
const testQenv = new Qenv('./', './.nogit');
import * as mailgun from '../ts/index.js';
import { type IMailgunMessage } from '../ts/index.js';
let testMailgunAccount: mailgun.MailgunAccount;
let testSmartmail: smartmail.Smartmail<IMailgunMessage>;
tap.test('should create a mailgun account', async () => {
testMailgunAccount = new mailgun.MailgunAccount({
apiToken: await testQenv.getEnvVarOnDemand('MAILGUN_API_TOKEN'),
region: 'eu',
});
await testMailgunAccount.addSmtpCredentials(
await testQenv.getEnvVarOnDemand('MAILGUN_SMTP_CREDENTIALS'),
);
expect(testMailgunAccount).toBeInstanceOf(mailgun.MailgunAccount);
});
tap.test('should create a smartmail', async () => {
testSmartmail = new smartmail.Smartmail({
body: 'hi there. This is the body.',
from: 'Lossless GmbH <noreply@mail.lossless.com>',
subject: 'TestMessage from @mojoio/mailgun test',
});
expect(testSmartmail).toBeInstanceOf(smartmail.Smartmail);
});
tap.test('should send a smartmail', async () => {
await testMailgunAccount.sendSmartMail(
testSmartmail,
'Sandbox Team <sandbox@mail.git.zone>',
);
});
tap.test('should send a smartmail with empty body', async () => {
const emptyBodySmartmail = new smartmail.Smartmail<IMailgunMessage>({
body: '',
from: 'Lossless GmbH <noreply@mail.lossless.one>',
subject: 'A message with no body from @mojoio/mailgun test',
});
await testMailgunAccount.sendSmartMail(
emptyBodySmartmail,
'Sandbox Team <sandbox@mail.git.zone>',
);
});
tap.test('should retrieve a mail using a retrieval url', async () => {
const result = await testMailgunAccount.retrieveSmartMailFromMessageUrl(
'https://sw.api.mailgun.net/v3/domains/mail.lossless.one/messages/AgMFnnnAKC8xp_dDa79LyoxhloxtaVmnRA==',
);
console.log(result);
// TODO handle empty body
if (false) {
result.options.subject = 'hi there. This is a testmail with attachment';
result.options.from = 'noreply@mail.lossless.com';
await testMailgunAccount.sendSmartMail(
result,
'Sandbox Team <sandbox@mail.git.zone>',
);
}
});
tap.start();

8
ts/00_commitinfo_data.ts Normal file
View File

@@ -0,0 +1,8 @@
/**
* autocreated commitinfo by @push.rocks/commitinfo
*/
export const commitinfo = {
name: '@apiclient.xyz/mailgun',
version: '2.0.2',
description: 'an api abstraction package for mailgun'
}

2
ts/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export * from './mailgun.classes.account.js';
export * from './interfaces/index.js';

2
ts/interfaces/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export * from './message.js';
export * from './notification.js';

30
ts/interfaces/message.ts Normal file
View File

@@ -0,0 +1,30 @@
export interface IMailgunMessage {
Received: string;
From: string;
'X-Envelope-From': string;
recipients: string;
'X-Google-Dkim-Signature': string;
To: string;
'message-headers': [[string, string]];
'Dkim-Signature': string;
'content-id-map': any;
subject: 'test2';
'stripped-html': string;
'X-Mailgun-Incoming': 'Yes';
'X-Received': string;
'X-Gm-Message-State': string;
'body-plain': string;
attachments: Array<{
url: string;
'content-type': string;
name: string;
size: number;
}>;
'body-html': string;
'Mime-Version': string;
Date: string;
'Message-Id': string;
'Content-Type': string;
'X-Google-Smtp-Source': string;
Subject: string;
}

View File

@@ -0,0 +1,47 @@
export interface IMailgunNotification {
timestamp: string;
token: string;
signature: string;
domain: string;
'Received-Spf': string;
From: string;
'Return-Path': string;
'Arc-Seal': string[];
'Delivered-To': string;
'X-Google-Dkim-Signature': string;
To: string;
'Dkim-Signature': string;
subject: string;
from: string;
'X-Received': string[];
'Ironport-Sdr': string;
'Arc-Authentication-Results': string[];
'Arc-Message-Signature': string[];
Date: string;
'Message-Id': string;
'Mime-Version': string;
Received: string[];
'Authentication-Results': string[];
'X-Ipas-Result': string;
'message-url': string;
'message-headers': string;
'Reply-To': string;
recipient: string;
sender: string;
'X-Mailgun-Incoming': string;
'X-Forwarded-For': string;
'X-Gm-Message-State': string;
'X-Google-Smtp-Source': string;
'X-Envelope-From': string;
'Content-Type': string;
'X-Forwarded-To': string;
Subject: string;
attachments: string;
'body-plain': string;
'stripped-text': string;
'stripped-html': string;
'stripped-signature': string;
// Lossless specific
'X-Lossless-Auth': string;
}

View File

@@ -0,0 +1,195 @@
import * as plugins from './mailgun.plugins.js';
import * as interfaces from './interfaces/index.js';
export interface IMailgunAccountContructorOptions {
apiToken: string;
region: 'eu' | 'us';
}
export class MailgunAccount {
public apiBaseUrl: string;
public options: IMailgunAccountContructorOptions;
public smartSmtps: { [domain: string]: plugins.smartsmtp.Smartsmtp } = {};
constructor(optionsArg: IMailgunAccountContructorOptions) {
this.options = optionsArg;
this.apiBaseUrl =
this.options.region === 'eu'
? 'https://api.eu.mailgun.net/v3'
: 'https://api..mailgun.net/v3';
}
/**
* allows adding smtp credentials
* Format: [domain]|[username]|[password]
*/
public async addSmtpCredentials(credentials: string) {
const credentialArray = credentials.split('|');
if (credentialArray.length !== 3) {
throw new Error('credentials are in the wrong format');
}
this.smartSmtps[credentialArray[0]] =
await plugins.smartsmtp.Smartsmtp.createSmartsmtpWithRelay({
smtpServer: 'smtp.eu.mailgun.org',
smtpUser: credentialArray[1],
smtpPassword: credentialArray[2],
});
}
public async getRequest(routeArg: string, binaryArg: boolean = false) {
let requestUrl = routeArg;
const needsBaseUrlPrefix = !routeArg.startsWith('https://');
if (needsBaseUrlPrefix) {
requestUrl = `${this.apiBaseUrl}${routeArg}`;
}
console.log(requestUrl);
const response = await plugins.smartrequest.SmartRequest.create()
.url(requestUrl)
.headers({
Authorization: `Basic ${plugins.smartstring.base64.encode(`api:${this.options.apiToken}`)}`,
'Content-Type': 'application/json',
})
.options({ keepAlive: false })
.get();
return response;
}
public async postFormData(
routeArg: string,
formFields: plugins.smartrequest.FormField[],
) {
const requestUrl = `${this.apiBaseUrl}${routeArg}`;
console.log(requestUrl);
const response = await plugins.smartrequest.SmartRequest.create()
.url(requestUrl)
.headers({
Authorization: `Basic ${plugins.smartstring.base64.encode(
`api:${this.options.apiToken}`,
)}`,
})
.options({ keepAlive: false })
.formData(formFields)
.post();
return response;
}
/**
* sends a SmartMail
*/
public async sendSmartMail(
smartmailArg: plugins.smartmail.Smartmail<interfaces.IMailgunMessage>,
toArg: string,
dataArg = {},
) {
const domain = smartmailArg.options.from.split('@')[1].replace('>', '');
const formFields: plugins.smartrequest.FormField[] = [
{
name: 'from',
value: smartmailArg.options.from,
},
{
name: 'to',
value: toArg,
},
{
name: 'subject',
value: smartmailArg.getSubject(dataArg),
},
{
name: 'html',
value: smartmailArg.getBody(dataArg),
},
];
console.log(smartmailArg.attachments);
for (const attachment of smartmailArg.attachments) {
formFields.push({
name: 'attachment',
value: attachment.contentBuffer,
filename: attachment.parsedPath.base,
});
}
if (smartmailArg.getBody(dataArg)) {
console.log('All requirements for API met');
const response = await this.postFormData(
`/${domain}/messages`,
formFields,
);
if (response.status === 200) {
console.log(
`Sent mail with subject ${smartmailArg.getSubject(
dataArg,
)} to ${toArg} using the mailgun API`,
);
return await response.json();
} else {
console.log(await response.json());
throw new Error('could not send email');
}
} else {
console.log(
'An empty body was provided. This does not work via the API, but using SMTP instead.',
);
const wantedSmartsmtp = this.smartSmtps[domain];
if (!wantedSmartsmtp) {
console.log('did not find appropriate smtp credentials');
return;
}
await wantedSmartsmtp.sendSmartMail(smartmailArg, toArg);
console.log(
`Sent mail with subject "${smartmailArg.getSubject(
dataArg,
)}" to "${toArg}" using an smtp transport over Mailgun.`,
);
}
}
public async retrieveSmartMailFromMessageUrl(messageUrlArg: string) {
console.log(`retrieving message for ${messageUrlArg}`);
const response = await this.getRequest(messageUrlArg);
if (response.status === 404) {
const body: any = await response.json();
console.log(body.message);
return null;
}
const responseBody: interfaces.IMailgunMessage = await response.json();
const smartmail =
new plugins.smartmail.Smartmail<interfaces.IMailgunMessage>({
from: responseBody.From,
body: responseBody['body-html'],
subject: responseBody.Subject,
creationObjectRef: responseBody,
});
// lets care about attachments
if (responseBody.attachments && responseBody.attachments instanceof Array) {
for (const attachmentInfo of responseBody.attachments) {
const attachmentName = attachmentInfo.name;
const attachmentContents = await this.getRequest(
attachmentInfo.url,
true,
);
const arrayBuffer = await attachmentContents.arrayBuffer();
smartmail.addAttachment(
new plugins.smartfile.SmartFile({
path: `./${attachmentName}`,
base: `./${attachmentName}`,
contentBuffer: Buffer.from(arrayBuffer),
}),
);
}
}
return smartmail;
}
public async retrieveSmartMailFromNotifyPayload(notifyPayloadArg: any) {
return await this.retrieveSmartMailFromMessageUrl(
notifyPayloadArg['message-url'],
);
}
}

8
ts/mailgun.plugins.ts Normal file
View File

@@ -0,0 +1,8 @@
// @pushrocks scope
import * as smartfile from '@push.rocks/smartfile';
import * as smartmail from '@push.rocks/smartmail';
import * as smartrequest from '@push.rocks/smartrequest';
import * as smartsmtp from '@push.rocks/smartsmtp';
import * as smartstring from '@push.rocks/smartstring';
export { smartfile, smartmail, smartrequest, smartsmtp, smartstring };

15
tsconfig.json Normal file
View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"useDefineForClassFields": false,
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"verbatimModuleSyntax": true,
"baseUrl": ".",
"paths": {}
},
"exclude": ["dist_*/**/*.d.ts"]
}

View File

@@ -1,3 +0,0 @@
{
"extends": "tslint-config-standard"
}