mirror of
https://github.com/dawidd6/action-send-mail.git
synced 2025-07-01 12:43:15 +07:00
Compare commits
12 Commits
Author | SHA1 | Date | |
---|---|---|---|
6063705cef | |||
64262eed9b | |||
118894614c | |||
07d4810b3b | |||
924d1fedb2 | |||
602f9d1725 | |||
6d23605227 | |||
f5b1987fb0 | |||
9020e79ee7 | |||
ba46013833 | |||
fd76768b61 | |||
4ef78fc181 |
16
.github/workflows/test.yml
vendored
16
.github/workflows/test.yml
vendored
@ -58,3 +58,19 @@ jobs:
|
|||||||
attachments: ${{matrix.attachments}}
|
attachments: ${{matrix.attachments}}
|
||||||
convert_markdown: ${{matrix.convert_markdown}}
|
convert_markdown: ${{matrix.convert_markdown}}
|
||||||
priority: high
|
priority: high
|
||||||
|
|
||||||
|
url-test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Send mail
|
||||||
|
uses: ./
|
||||||
|
with:
|
||||||
|
connection_url: smtp+starttls://${{secrets.USERNAME}}:${{secrets.PASSWORD}}@${{secrets.ADDRESS}}/
|
||||||
|
subject: Plain body with connection_url
|
||||||
|
body: |
|
||||||
|
first line
|
||||||
|
second line
|
||||||
|
to: ${{github.event.pusher.email}}
|
||||||
|
from: github-actions
|
||||||
|
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
.idea
|
29
README.md
29
README.md
@ -16,10 +16,20 @@ Some features:
|
|||||||
- name: Send mail
|
- name: Send mail
|
||||||
uses: dawidd6/action-send-mail@v3
|
uses: dawidd6/action-send-mail@v3
|
||||||
with:
|
with:
|
||||||
# Required mail server address:
|
# Specify connection via URL (replaces server_address, server_port, secure,
|
||||||
|
# username and password)
|
||||||
|
#
|
||||||
|
# Format:
|
||||||
|
#
|
||||||
|
# * smtp://user:password@server:port
|
||||||
|
# * smtp+starttls://user:password@server:port
|
||||||
|
connection_url: ${{secrets.MAIL_CONNECTION}}
|
||||||
|
# Required mail server address if not connection_url:
|
||||||
server_address: smtp.gmail.com
|
server_address: smtp.gmail.com
|
||||||
# Required mail server port:
|
# Server port, default 25:
|
||||||
server_port: 465
|
server_port: 465
|
||||||
|
# Optional whether this connection use TLS (default is true if server_port is 465)
|
||||||
|
secure: true
|
||||||
# Optional (recommended): mail server username:
|
# Optional (recommended): mail server username:
|
||||||
username: ${{secrets.MAIL_USERNAME}}
|
username: ${{secrets.MAIL_USERNAME}}
|
||||||
# Optional (recommended) mail server password:
|
# Optional (recommended) mail server password:
|
||||||
@ -30,8 +40,6 @@ Some features:
|
|||||||
to: obiwan@example.com,yoda@example.com
|
to: obiwan@example.com,yoda@example.com
|
||||||
# Required sender full name (address can be skipped):
|
# Required sender full name (address can be skipped):
|
||||||
from: Luke Skywalker # <user@example.com>
|
from: Luke Skywalker # <user@example.com>
|
||||||
# Optional whether this connection use TLS (default is true if server_port is 465)
|
|
||||||
secure: true
|
|
||||||
# Optional plain body:
|
# Optional plain body:
|
||||||
body: Build job of ${{github.repository}} completed successfully!
|
body: Build job of ${{github.repository}} completed successfully!
|
||||||
# Optional HTML body read from file:
|
# Optional HTML body read from file:
|
||||||
@ -42,6 +50,8 @@ Some features:
|
|||||||
bcc: r2d2@example.com,hansolo@example.com
|
bcc: r2d2@example.com,hansolo@example.com
|
||||||
# Optional recipient of the email response:
|
# Optional recipient of the email response:
|
||||||
reply_to: luke@example.com
|
reply_to: luke@example.com
|
||||||
|
# Optional Message ID this message is replying to:
|
||||||
|
in_reply_to: <random-luke@example.com>
|
||||||
# Optional unsigned/invalid certificates allowance:
|
# Optional unsigned/invalid certificates allowance:
|
||||||
ignore_cert: true
|
ignore_cert: true
|
||||||
# Optional converting Markdown to HTML (set content_type to text/html too):
|
# Optional converting Markdown to HTML (set content_type to text/html too):
|
||||||
@ -56,14 +66,11 @@ Some features:
|
|||||||
|
|
||||||
### Gmail
|
### Gmail
|
||||||
|
|
||||||
Gmail security settings may cause this Action to fail. This failure may involve a message in the GitHub Actions details about access being denied and an email from Google to the email account being used about a sign-in being blocked and why.
|
Instead of using your normal Google password, use an App password.
|
||||||
|
|
||||||
Changes in Gmail settings may be necessary to get this action to work.
|
1. [Enable 2-Step Verification.](https://support.google.com/accounts/answer/185839?hl=en&co=GENIE.Platform%3DAndroid).
|
||||||
1. Google treats this method of using email as a "Less Secure App". However, "Less Secure Apps" can be enabled in Google profile settings. There doesn't appear to be a static link for this, but if you go to Google profile settings while signed-in and type "less secure apps" into the search bar, the appropriate instructions will come up.
|
This is needed to create an App password.
|
||||||
2. IMAP needs to be enabled in Gmail settings as described [here](https://support.google.com/mail/answer/7126229?hl=en).
|
2. [Create an App password](https://support.google.com/accounts/answer/185833?hl=en) for `Mail`.
|
||||||
3. If the Gmail account you're trying to use in this Action is already 2FA (Two Factor Authentication) enabled, the 2FA password will need to be provided as well, which isn't included in the default template.
|
|
||||||
|
|
||||||
Users who have had problems have reported success by doing each of these three steps or by doing the first two steps and using a Gmail account that didn't have 2FA enabled.
|
|
||||||
|
|
||||||
### Unauthenticated login (username/password fields)
|
### Unauthenticated login (username/password fields)
|
||||||
|
|
||||||
|
17
action.yml
17
action.yml
@ -5,18 +5,19 @@ branding:
|
|||||||
icon: mail
|
icon: mail
|
||||||
color: blue
|
color: blue
|
||||||
inputs:
|
inputs:
|
||||||
|
connection_url:
|
||||||
|
description: Connection URL protocol://user:password@server:port, protocol can be smtp or smtp+starttls, replaces server_address, server_port, secure, username and password
|
||||||
server_address:
|
server_address:
|
||||||
description: SMTP server address
|
description: SMTP server address
|
||||||
required: true
|
|
||||||
server_port:
|
server_port:
|
||||||
description: SMTP server port
|
description: SMTP server port
|
||||||
required: true
|
default: "25"
|
||||||
|
secure:
|
||||||
|
description: Whether this connection use TLS (default is true if server_port is 465)
|
||||||
username:
|
username:
|
||||||
description: Authenticate as this user to SMTP server
|
description: Authenticate as this user to SMTP server
|
||||||
required: false
|
|
||||||
password:
|
password:
|
||||||
description: Authenticate with this password to SMTP server
|
description: Authenticate with this password to SMTP server
|
||||||
required: false
|
|
||||||
subject:
|
subject:
|
||||||
description: Subject of mail message
|
description: Subject of mail message
|
||||||
required: true
|
required: true
|
||||||
@ -26,9 +27,6 @@ inputs:
|
|||||||
from:
|
from:
|
||||||
description: Full name of mail sender (might be with an email address specified in <>)
|
description: Full name of mail sender (might be with an email address specified in <>)
|
||||||
required: true
|
required: true
|
||||||
secure:
|
|
||||||
description: Whether this connection use TLS (default is true if server_port is 465)
|
|
||||||
required: false
|
|
||||||
body:
|
body:
|
||||||
description: Body of mail message (might be a filename prefixed with file:// to read from)
|
description: Body of mail message (might be a filename prefixed with file:// to read from)
|
||||||
required: false
|
required: false
|
||||||
@ -44,6 +42,9 @@ inputs:
|
|||||||
reply_to:
|
reply_to:
|
||||||
description: An email address that will appear on the Reply-To field
|
description: An email address that will appear on the Reply-To field
|
||||||
required: false
|
required: false
|
||||||
|
in_reply_to:
|
||||||
|
description: The Message-ID this message is replying to
|
||||||
|
required: false
|
||||||
ignore_cert:
|
ignore_cert:
|
||||||
description: Allow unsigned/invalid certificates
|
description: Allow unsigned/invalid certificates
|
||||||
required: false
|
required: false
|
||||||
@ -57,5 +58,5 @@ inputs:
|
|||||||
description: Set Priority level for the mail message to 'high', 'normal' (default) or 'low'
|
description: Set Priority level for the mail message to 'high', 'normal' (default) or 'low'
|
||||||
required: false
|
required: false
|
||||||
runs:
|
runs:
|
||||||
using: node12
|
using: node16
|
||||||
main: main.js
|
main: main.js
|
||||||
|
56
main.js
56
main.js
@ -3,6 +3,7 @@ const core = require("@actions/core")
|
|||||||
const glob = require("@actions/glob")
|
const glob = require("@actions/glob")
|
||||||
const fs = require("fs")
|
const fs = require("fs")
|
||||||
const showdown = require("showdown")
|
const showdown = require("showdown")
|
||||||
|
const path = require("path")
|
||||||
|
|
||||||
function getBody(bodyOrFile, convertMarkdown) {
|
function getBody(bodyOrFile, convertMarkdown) {
|
||||||
let body = bodyOrFile
|
let body = bodyOrFile
|
||||||
@ -33,29 +34,68 @@ function getFrom(from, username) {
|
|||||||
async function getAttachments(attachments) {
|
async function getAttachments(attachments) {
|
||||||
const globber = await glob.create(attachments.split(',').join('\n'))
|
const globber = await glob.create(attachments.split(',').join('\n'))
|
||||||
const files = await globber.glob()
|
const files = await globber.glob()
|
||||||
return files.map(f => ({ path: f }))
|
return files.map(f => ({ filename: path.basename(f), path: f, cid: f.replace(/^.*[\\\/]/, '')}))
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
try {
|
try {
|
||||||
const serverAddress = core.getInput("server_address", { required: true })
|
let serverAddress = core.getInput("server_address")
|
||||||
const serverPort = core.getInput("server_port", { required: true })
|
let serverPort = core.getInput("server_port")
|
||||||
const username = core.getInput("username")
|
let secure = core.getInput("secure")
|
||||||
const password = core.getInput("password")
|
let username = core.getInput("username")
|
||||||
|
let password = core.getInput("password")
|
||||||
|
|
||||||
|
if (!secure) {
|
||||||
|
secure = serverPort === "465" ? "true" : "false"
|
||||||
|
}
|
||||||
|
|
||||||
|
const connectionUrl = core.getInput("connection_url")
|
||||||
|
if (connectionUrl) {
|
||||||
|
const url = new URL(connectionUrl)
|
||||||
|
switch (url.protocol) {
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported connection protocol '${url.protocol}'`)
|
||||||
|
case "smtp:":
|
||||||
|
serverPort = "25"
|
||||||
|
secure = "false"
|
||||||
|
break
|
||||||
|
case "smtp+starttls:":
|
||||||
|
serverPort = "465"
|
||||||
|
secure = "true"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (url.hostname) {
|
||||||
|
serverAddress = url.hostname
|
||||||
|
}
|
||||||
|
if (url.port) {
|
||||||
|
serverPort = url.port
|
||||||
|
}
|
||||||
|
if (url.username) {
|
||||||
|
username = unescape(url.username)
|
||||||
|
}
|
||||||
|
if (url.password) {
|
||||||
|
password = unescape(url.password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const subject = core.getInput("subject", { required: true })
|
const subject = core.getInput("subject", { required: true })
|
||||||
const from = core.getInput("from", { required: true })
|
const from = core.getInput("from", { required: true })
|
||||||
const to = core.getInput("to", { required: true })
|
const to = core.getInput("to", { required: true })
|
||||||
const secure = core.getInput("secure", { required: false })
|
|
||||||
const body = core.getInput("body", { required: false })
|
const body = core.getInput("body", { required: false })
|
||||||
const htmlBody = core.getInput("html_body", { required: false })
|
const htmlBody = core.getInput("html_body", { required: false })
|
||||||
const cc = core.getInput("cc", { required: false })
|
const cc = core.getInput("cc", { required: false })
|
||||||
const bcc = core.getInput("bcc", { required: false })
|
const bcc = core.getInput("bcc", { required: false })
|
||||||
const replyTo = core.getInput("reply_to", { required: false })
|
const replyTo = core.getInput("reply_to", { required: false })
|
||||||
|
const inReplyTo = core.getInput("in_reply_to", { required: false })
|
||||||
const attachments = core.getInput("attachments", { required: false })
|
const attachments = core.getInput("attachments", { required: false })
|
||||||
const convertMarkdown = core.getInput("convert_markdown", { required: false })
|
const convertMarkdown = core.getInput("convert_markdown", { required: false })
|
||||||
const ignoreCert = core.getInput("ignore_cert", { required: false })
|
const ignoreCert = core.getInput("ignore_cert", { required: false })
|
||||||
const priority = core.getInput("priority", { required: false })
|
const priority = core.getInput("priority", { required: false })
|
||||||
|
|
||||||
|
if (!serverAddress) {
|
||||||
|
throw new Error("Server address must be specified")
|
||||||
|
}
|
||||||
|
|
||||||
if (!username || !password) {
|
if (!username || !password) {
|
||||||
core.warning("Username and password not specified. You should only do this if you are using a self-hosted runner to access an on-premise mail server.")
|
core.warning("Username and password not specified. You should only do this if you are using a self-hosted runner to access an on-premise mail server.")
|
||||||
}
|
}
|
||||||
@ -67,7 +107,7 @@ async function main() {
|
|||||||
pass: password
|
pass: password
|
||||||
} : undefined,
|
} : undefined,
|
||||||
port: serverPort,
|
port: serverPort,
|
||||||
secure: secure == "true" ? true : serverPort == "465",
|
secure: secure === "true",
|
||||||
tls: ignoreCert == "true" ? {
|
tls: ignoreCert == "true" ? {
|
||||||
rejectUnauthorized: false
|
rejectUnauthorized: false
|
||||||
} : undefined,
|
} : undefined,
|
||||||
@ -80,6 +120,8 @@ async function main() {
|
|||||||
cc: cc ? cc : undefined,
|
cc: cc ? cc : undefined,
|
||||||
bcc: bcc ? bcc : undefined,
|
bcc: bcc ? bcc : undefined,
|
||||||
replyTo: replyTo ? replyTo : undefined,
|
replyTo: replyTo ? replyTo : undefined,
|
||||||
|
inReplyTo: inReplyTo ? inReplyTo : undefined,
|
||||||
|
references: inReplyTo ? inReplyTo : undefined,
|
||||||
text: body ? getBody(body, false) : undefined,
|
text: body ? getBody(body, false) : undefined,
|
||||||
html: htmlBody ? getBody(htmlBody, convertMarkdown) : undefined,
|
html: htmlBody ? getBody(htmlBody, convertMarkdown) : undefined,
|
||||||
priority: priority ? priority : undefined,
|
priority: priority ? priority : undefined,
|
||||||
|
40
package-lock.json
generated
40
package-lock.json
generated
@ -6,8 +6,8 @@
|
|||||||
"": {
|
"": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.2.7",
|
"@actions/core": "^1.2.7",
|
||||||
"@actions/glob": "^0.2.0",
|
"@actions/glob": "^0.2.1",
|
||||||
"nodemailer": "^6.4.17",
|
"nodemailer": "^6.7.8",
|
||||||
"showdown": "^1.9.1"
|
"showdown": "^1.9.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -17,18 +17,18 @@
|
|||||||
"integrity": "sha512-CGx2ilGq5i7zSLgiiGUtBCxhRRxibJYU6Fim0Q1Wg2aQL2LTnF27zbqZOrxfvFQ55eSBW0L8uVStgtKMpa0Qlg=="
|
"integrity": "sha512-CGx2ilGq5i7zSLgiiGUtBCxhRRxibJYU6Fim0Q1Wg2aQL2LTnF27zbqZOrxfvFQ55eSBW0L8uVStgtKMpa0Qlg=="
|
||||||
},
|
},
|
||||||
"node_modules/@actions/glob": {
|
"node_modules/@actions/glob": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@actions/glob/-/glob-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@actions/glob/-/glob-0.2.1.tgz",
|
||||||
"integrity": "sha512-mqE2a7I66kxcvsdwxs/filQwZsq25IfktMaviGfDB51v6Q3bvxnV7mFsZnvYtLhqGZbPxwBnH8AD3UYaOWb//w==",
|
"integrity": "sha512-OqseGbxR8vVikg6rfdKST21GX3QYGq2Nz7/gX3UxZb2Mw1ujJ2S3U5CsYUvYHwxbYguU+HNhfE3930oo5nprXQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.2.6",
|
"@actions/core": "^1.2.6",
|
||||||
"minimatch": "^3.0.4"
|
"minimatch": "^3.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ansi-regex": {
|
"node_modules/ansi-regex": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz",
|
||||||
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
|
"integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
@ -158,9 +158,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nodemailer": {
|
"node_modules/nodemailer": {
|
||||||
"version": "6.6.2",
|
"version": "6.7.8",
|
||||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.7.8.tgz",
|
||||||
"integrity": "sha512-YSzu7TLbI+bsjCis/TZlAXBoM4y93HhlIgo0P5oiA2ua9Z4k+E2Fod//ybIzdJxOlXGRcHIh/WaeCBehvxZb/Q==",
|
"integrity": "sha512-2zaTFGqZixVmTxpJRCFC+Vk5eGRd/fYtvIR+dl5u9QXLTQWGIf48x/JXvo58g9sa0bU6To04XUv554Paykum3g==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
@ -317,18 +317,18 @@
|
|||||||
"integrity": "sha512-CGx2ilGq5i7zSLgiiGUtBCxhRRxibJYU6Fim0Q1Wg2aQL2LTnF27zbqZOrxfvFQ55eSBW0L8uVStgtKMpa0Qlg=="
|
"integrity": "sha512-CGx2ilGq5i7zSLgiiGUtBCxhRRxibJYU6Fim0Q1Wg2aQL2LTnF27zbqZOrxfvFQ55eSBW0L8uVStgtKMpa0Qlg=="
|
||||||
},
|
},
|
||||||
"@actions/glob": {
|
"@actions/glob": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@actions/glob/-/glob-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@actions/glob/-/glob-0.2.1.tgz",
|
||||||
"integrity": "sha512-mqE2a7I66kxcvsdwxs/filQwZsq25IfktMaviGfDB51v6Q3bvxnV7mFsZnvYtLhqGZbPxwBnH8AD3UYaOWb//w==",
|
"integrity": "sha512-OqseGbxR8vVikg6rfdKST21GX3QYGq2Nz7/gX3UxZb2Mw1ujJ2S3U5CsYUvYHwxbYguU+HNhfE3930oo5nprXQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@actions/core": "^1.2.6",
|
"@actions/core": "^1.2.6",
|
||||||
"minimatch": "^3.0.4"
|
"minimatch": "^3.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ansi-regex": {
|
"ansi-regex": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz",
|
||||||
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
|
"integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g=="
|
||||||
},
|
},
|
||||||
"ansi-styles": {
|
"ansi-styles": {
|
||||||
"version": "3.2.1",
|
"version": "3.2.1",
|
||||||
@ -431,9 +431,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nodemailer": {
|
"nodemailer": {
|
||||||
"version": "6.6.2",
|
"version": "6.7.8",
|
||||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.7.8.tgz",
|
||||||
"integrity": "sha512-YSzu7TLbI+bsjCis/TZlAXBoM4y93HhlIgo0P5oiA2ua9Z4k+E2Fod//ybIzdJxOlXGRcHIh/WaeCBehvxZb/Q=="
|
"integrity": "sha512-2zaTFGqZixVmTxpJRCFC+Vk5eGRd/fYtvIR+dl5u9QXLTQWGIf48x/JXvo58g9sa0bU6To04XUv554Paykum3g=="
|
||||||
},
|
},
|
||||||
"p-limit": {
|
"p-limit": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
|
@ -3,8 +3,8 @@
|
|||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.2.7",
|
"@actions/core": "^1.2.7",
|
||||||
"@actions/glob": "^0.2.0",
|
"@actions/glob": "^0.2.1",
|
||||||
"nodemailer": "^6.4.17",
|
"nodemailer": "^6.7.8",
|
||||||
"showdown": "^1.9.1"
|
"showdown": "^1.9.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user