Projektstart
This commit is contained in:
6
backend/node_modules/imapflow/.babelrc
generated
vendored
Normal file
6
backend/node_modules/imapflow/.babelrc
generated
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"presets": ["@babel/env"],
|
||||
"plugins": [
|
||||
"@babel/plugin-syntax-class-properties"
|
||||
]
|
||||
}
|
||||
16
backend/node_modules/imapflow/.eslintrc
generated
vendored
Normal file
16
backend/node_modules/imapflow/.eslintrc
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"rules": {
|
||||
"no-await-in-loop": 0,
|
||||
"require-atomic-updates": 0
|
||||
},
|
||||
"globals": {
|
||||
"BigInt": true
|
||||
},
|
||||
"extends": ["nodemailer", "prettier"],
|
||||
"parser": "@babel/eslint-parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2020,
|
||||
"sourceType": "script"
|
||||
},
|
||||
"plugins": ["@babel"]
|
||||
}
|
||||
1
backend/node_modules/imapflow/.gitattributes
generated
vendored
Normal file
1
backend/node_modules/imapflow/.gitattributes
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.js text eol=lf
|
||||
76
backend/node_modules/imapflow/.github/CODE_OF_CONDUCT.md
generated
vendored
Normal file
76
backend/node_modules/imapflow/.github/CODE_OF_CONDUCT.md
generated
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
- Using welcoming and inclusive language
|
||||
- Being respectful of differing viewpoints and experiences
|
||||
- Gracefully accepting constructive criticism
|
||||
- Focusing on what is best for the community
|
||||
- Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
- The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
- Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at andris@postalsys.com. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
4
backend/node_modules/imapflow/.github/FUNDING.yml
generated
vendored
Normal file
4
backend/node_modules/imapflow/.github/FUNDING.yml
generated
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [andris9] # enable once enrolled
|
||||
custom: ['https://www.paypal.me/nodemailer']
|
||||
40
backend/node_modules/imapflow/.github/ISSUE_TEMPLATE/bug_report.md
generated
vendored
Normal file
40
backend/node_modules/imapflow/.github/ISSUE_TEMPLATE/bug_report.md
generated
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
19
backend/node_modules/imapflow/.github/ISSUE_TEMPLATE/feature_request.md
generated
vendored
Normal file
19
backend/node_modules/imapflow/.github/ISSUE_TEMPLATE/feature_request.md
generated
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
17
backend/node_modules/imapflow/.github/contributing.md
generated
vendored
Normal file
17
backend/node_modules/imapflow/.github/contributing.md
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
# Contributing
|
||||
|
||||
## Legal
|
||||
|
||||
(agreement below borrowed from [Sidekiq Legal](https://github.com/mperham/sidekiq/blob/master/.github/contributing.md))
|
||||
|
||||
By submitting a Pull Request, you disavow any rights or claims to any changes
|
||||
submitted to the ImapFlow project and assign the copyright of
|
||||
those changes to Postal Systems OÜ.
|
||||
|
||||
If you cannot or do not want to reassign those rights (your employment
|
||||
contract for your employer may not allow this), you should not submit a PR.
|
||||
Open an issue and someone else can do the work.
|
||||
|
||||
This is a legal way of saying "If you submit a PR to us, that code becomes ours".
|
||||
99.9% of the time that's what you intend anyways; we hope it doesn't scare you
|
||||
away from contributing.
|
||||
36
backend/node_modules/imapflow/.github/workflows/release.yaml
generated
vendored
Normal file
36
backend/node_modules/imapflow/.github/workflows/release.yaml
generated
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
id-token: write
|
||||
|
||||
name: release
|
||||
jobs:
|
||||
release-please:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
release_created: ${{ steps.release.outputs.release_created }}
|
||||
steps:
|
||||
- uses: googleapis/release-please-action@v4
|
||||
id: release
|
||||
|
||||
publish:
|
||||
needs: release-please
|
||||
if: ${{ needs.release-please.outputs.release_created }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- run: npm ci
|
||||
- run: npm publish --provenance --access public
|
||||
24
backend/node_modules/imapflow/.github/workflows/stale.yml
generated
vendored
Normal file
24
backend/node_modules/imapflow/.github/workflows/stale.yml
generated
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
name: 'Close stale issues and PRs'
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 1 * * *'
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v8
|
||||
with:
|
||||
stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
|
||||
stale-pr-message: 'This PR is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 10 days.'
|
||||
close-issue-message: 'This issue was closed because it has been stalled for 15 days with no activity.'
|
||||
close-pr-message: 'This PR was closed because it has been stalled for 20 days with no activity.'
|
||||
days-before-issue-stale: 30
|
||||
days-before-pr-stale: 45
|
||||
days-before-issue-close: 15
|
||||
days-before-pr-close: 20
|
||||
stale-issue-label: 'no-issue-activity'
|
||||
exempt-issue-labels: 'pending,work-in-progress'
|
||||
stale-pr-label: 'no-pr-activity'
|
||||
exempt-pr-labels: 'awaiting-approval,work-in-progress'
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
21
backend/node_modules/imapflow/.github/workflows/test.yml
generated
vendored
Normal file
21
backend/node_modules/imapflow/.github/workflows/test.yml
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: Run tests
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
node: [20.x, 22.x, 24.x]
|
||||
os: [ubuntu-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js ${{ matrix.node }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
- run: npm install
|
||||
- run: npm test
|
||||
4
backend/node_modules/imapflow/.ncurc.js
generated
vendored
Normal file
4
backend/node_modules/imapflow/.ncurc.js
generated
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
upgrade: true,
|
||||
reject: ['jsdoc', 'grunt-eslint']
|
||||
};
|
||||
4
backend/node_modules/imapflow/.prettierignore
generated
vendored
Normal file
4
backend/node_modules/imapflow/.prettierignore
generated
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
package-lock.json
|
||||
CHANGELOG.md
|
||||
docs/
|
||||
8
backend/node_modules/imapflow/.prettierrc.js
generated
vendored
Normal file
8
backend/node_modules/imapflow/.prettierrc.js
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
printWidth: 160,
|
||||
tabWidth: 4,
|
||||
singleQuote: true,
|
||||
endOfLine: 'lf',
|
||||
trailingComma: 'none',
|
||||
arrowParens: 'avoid'
|
||||
};
|
||||
3
backend/node_modules/imapflow/.release-please-manifest.json
generated
vendored
Normal file
3
backend/node_modules/imapflow/.release-please-manifest.json
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
".": "1.2.6"
|
||||
}
|
||||
1030
backend/node_modules/imapflow/CHANGELOG.md
generated
vendored
Normal file
1030
backend/node_modules/imapflow/CHANGELOG.md
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
21
backend/node_modules/imapflow/Gruntfile.js
generated
vendored
Normal file
21
backend/node_modules/imapflow/Gruntfile.js
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = function (grunt) {
|
||||
// Project configuration.
|
||||
grunt.initConfig({
|
||||
eslint: {
|
||||
all: ['lib/**/*.js', 'test/**/*.js', 'Gruntfile.js']
|
||||
},
|
||||
|
||||
nodeunit: {
|
||||
all: ['test/**/*-test.js']
|
||||
}
|
||||
});
|
||||
|
||||
// Load the plugin(s)
|
||||
grunt.loadNpmTasks('grunt-eslint');
|
||||
grunt.loadNpmTasks('grunt-contrib-nodeunit');
|
||||
|
||||
// Tasks
|
||||
grunt.registerTask('default', ['eslint', 'nodeunit']);
|
||||
};
|
||||
16
backend/node_modules/imapflow/LICENSE.txt
generated
vendored
Normal file
16
backend/node_modules/imapflow/LICENSE.txt
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
Copyright (c) 2020-2024 Postal Systems OÜ
|
||||
|
||||
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 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.
|
||||
109
backend/node_modules/imapflow/README.md
generated
vendored
Normal file
109
backend/node_modules/imapflow/README.md
generated
vendored
Normal file
@@ -0,0 +1,109 @@
|
||||
# ImapFlow
|
||||
|
||||
ImapFlow is a modern and easy-to-use IMAP client library for Node.js.
|
||||
|
||||
> [!NOTE]
|
||||
> Managing an IMAP connection is cool, but if you are only looking for an easy way to integrate email accounts, then ImapFlow was built for [EmailEngine Email API](https://emailengine.app/). It's a self-hosted software that converts all IMAP accounts to easy-to-use REST interfaces.
|
||||
|
||||
The focus for ImapFlow is to provide easy to use API over IMAP. Using ImapFlow does not expect knowledge about specific IMAP details. A general understanding is good enough.
|
||||
|
||||
IMAP extensions are handled in the background, so, for example, you can always request `labels` value from a {@link FetchQueryObject|fetch()} call, but if the IMAP server does not support `X-GM-EXT-1` extension, then `labels` value is not included in the response.
|
||||
|
||||
## Source
|
||||
|
||||
Source code is available from [Github](https://github.com/postalsys/imapflow).
|
||||
|
||||
## Usage
|
||||
|
||||
First install the module from npm:
|
||||
|
||||
```
|
||||
npm install imapflow
|
||||
```
|
||||
|
||||
next import the ImapFlow class into your script:
|
||||
|
||||
```js
|
||||
const { ImapFlow } = require('imapflow');
|
||||
```
|
||||
|
||||
### Promises
|
||||
|
||||
All ImapFlow methods use Promises, so you need to wait using `await` or wait for the `then()` method to fire until you get the response.
|
||||
|
||||
```js
|
||||
const { ImapFlow } = require('imapflow');
|
||||
const client = new ImapFlow({
|
||||
host: 'ethereal.email',
|
||||
port: 993,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: 'garland.mcclure71@ethereal.email',
|
||||
pass: 'mW6e4wWWnEd3H4hT5B'
|
||||
}
|
||||
});
|
||||
|
||||
const main = async () => {
|
||||
// Wait until client connects and authorizes
|
||||
await client.connect();
|
||||
|
||||
// Select and lock a mailbox. Throws if mailbox does not exist
|
||||
let lock = await client.getMailboxLock('INBOX');
|
||||
try {
|
||||
// fetch latest message source
|
||||
// client.mailbox includes information about currently selected mailbox
|
||||
// "exists" value is also the largest sequence number available in the mailbox
|
||||
let message = await client.fetchOne(client.mailbox.exists, { source: true });
|
||||
console.log(message.source.toString());
|
||||
|
||||
// list subjects for all messages
|
||||
// uid value is always included in FETCH response, envelope strings are in unicode.
|
||||
for await (let message of client.fetch('1:*', { envelope: true })) {
|
||||
console.log(`${message.uid}: ${message.envelope.subject}`);
|
||||
}
|
||||
} finally {
|
||||
// Make sure lock is released, otherwise next `getMailboxLock()` never returns
|
||||
lock.release();
|
||||
}
|
||||
|
||||
// log out and close connection
|
||||
await client.logout();
|
||||
};
|
||||
|
||||
main().catch(err => console.error(err));
|
||||
```
|
||||
|
||||
### Admin Impersonation / Delegation (SASL PLAIN with authzid)
|
||||
|
||||
ImapFlow supports admin impersonation for mail systems like Zimbra that allow administrators to access user mailboxes. This is done using the SASL PLAIN mechanism with an authorization identity (`authzid`).
|
||||
|
||||
```js
|
||||
const { ImapFlow } = require('imapflow');
|
||||
const client = new ImapFlow({
|
||||
host: 'mail.example.com',
|
||||
port: 993,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: 'admin@example.com', // Admin credentials (authentication identity)
|
||||
pass: 'adminpassword',
|
||||
authzid: 'user@example.com', // User to impersonate (authorization identity)
|
||||
loginMethod: 'AUTH=PLAIN' // Must use PLAIN mechanism for authzid
|
||||
}
|
||||
});
|
||||
|
||||
// Connection will authenticate as admin but authorize as the specified user
|
||||
await client.connect();
|
||||
// Now operating on user@example.com's mailbox as admin
|
||||
```
|
||||
|
||||
**Note:** The `authzid` parameter only works with the `AUTH=PLAIN` mechanism. The server must support admin delegation/impersonation for this to work.
|
||||
|
||||
## Documentation
|
||||
|
||||
[API reference](https://imapflow.com/module-imapflow-ImapFlow.html).
|
||||
|
||||
## License
|
||||
|
||||
© 2020-2024 Postal Systems OÜ
|
||||
|
||||
Licensed under **MIT-license**
|
||||
BIN
backend/node_modules/imapflow/assets/favicon.ico
generated
vendored
Normal file
BIN
backend/node_modules/imapflow/assets/favicon.ico
generated
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
43
backend/node_modules/imapflow/eslint.config.js
generated
vendored
Normal file
43
backend/node_modules/imapflow/eslint.config.js
generated
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
'use strict';
|
||||
|
||||
const { FlatCompat } = require('@eslint/eslintrc');
|
||||
const js = require('@eslint/js');
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended
|
||||
});
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
ignores: ['node_modules/**', 'examples/**', 'docs/**']
|
||||
},
|
||||
...compat.extends('nodemailer', 'prettier'),
|
||||
{
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'script',
|
||||
globals: {
|
||||
BigInt: 'readonly'
|
||||
},
|
||||
parser: require('@babel/eslint-parser'),
|
||||
parserOptions: {
|
||||
requireConfigFile: false
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
'@babel': require('@babel/eslint-plugin')
|
||||
},
|
||||
rules: {
|
||||
'no-await-in-loop': 0,
|
||||
'require-atomic-updates': 0
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['eslint.config.js', '.prettierrc.js', '.ncurc.js'],
|
||||
rules: {
|
||||
'global-require': 0,
|
||||
strict: 0
|
||||
}
|
||||
}
|
||||
];
|
||||
28
backend/node_modules/imapflow/jsdoc.json
generated
vendored
Normal file
28
backend/node_modules/imapflow/jsdoc.json
generated
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"templates": {
|
||||
"referenceTitle": "ImapFlow",
|
||||
"disableSort": false,
|
||||
"collapse": true,
|
||||
"resources": {
|
||||
"Source Code": "https://github.com/postalsys/imapflow"
|
||||
},
|
||||
"cleverLinks": true,
|
||||
"monospaceLinks": false,
|
||||
"default": {
|
||||
"outputSourceFiles": false
|
||||
},
|
||||
"search": {
|
||||
"apiKey": "082c6635b32d44ed095369a5f1c790fd",
|
||||
"indexName": "imapflow",
|
||||
"hitsPerPage": 7
|
||||
}
|
||||
},
|
||||
"plugins": ["plugins/markdown"],
|
||||
"opts": {
|
||||
"destination": "./docs/",
|
||||
"encoding": "utf8",
|
||||
"private": true,
|
||||
"recurse": true,
|
||||
"template": "./node_modules/imapflow-jsdoc-template"
|
||||
}
|
||||
}
|
||||
283
backend/node_modules/imapflow/lib/charsets.js
generated
vendored
Normal file
283
backend/node_modules/imapflow/lib/charsets.js
generated
vendored
Normal file
@@ -0,0 +1,283 @@
|
||||
'use strict';
|
||||
|
||||
const CHARACTER_SETS = [
|
||||
'US-ASCII',
|
||||
'ISO-8859-1',
|
||||
'ISO-8859-2',
|
||||
'ISO-8859-3',
|
||||
'ISO-8859-4',
|
||||
'ISO-8859-5',
|
||||
'ISO-8859-6',
|
||||
'ISO-8859-7',
|
||||
'ISO-8859-8',
|
||||
'ISO-8859-9',
|
||||
'ISO-8859-10',
|
||||
'ISO_6937-2-add',
|
||||
'JIS_X0201',
|
||||
'JIS_Encoding',
|
||||
'Shift_JIS',
|
||||
'EUC-JP',
|
||||
'Extended_UNIX_Code_Fixed_Width_for_Japanese',
|
||||
'BS_4730',
|
||||
'SEN_850200_C',
|
||||
'IT',
|
||||
'ES',
|
||||
'DIN_66003',
|
||||
'NS_4551-1',
|
||||
'NF_Z_62-010',
|
||||
'ISO-10646-UTF-1',
|
||||
'ISO_646.basic:1983',
|
||||
'INVARIANT',
|
||||
'ISO_646.irv:1983',
|
||||
'NATS-SEFI',
|
||||
'NATS-SEFI-ADD',
|
||||
'NATS-DANO',
|
||||
'NATS-DANO-ADD',
|
||||
'SEN_850200_B',
|
||||
'KS_C_5601-1987',
|
||||
'ISO-2022-KR',
|
||||
'EUC-KR',
|
||||
'ISO-2022-JP',
|
||||
'ISO-2022-JP-2',
|
||||
'JIS_C6220-1969-jp',
|
||||
'JIS_C6220-1969-ro',
|
||||
'PT',
|
||||
'greek7-old',
|
||||
'latin-greek',
|
||||
'NF_Z_62-010_(1973)',
|
||||
'Latin-greek-1',
|
||||
'ISO_5427',
|
||||
'JIS_C6226-1978',
|
||||
'BS_viewdata',
|
||||
'INIS',
|
||||
'INIS-8',
|
||||
'INIS-cyrillic',
|
||||
'ISO_5427:1981',
|
||||
'ISO_5428:1980',
|
||||
'GB_1988-80',
|
||||
'GB_2312-80',
|
||||
'NS_4551-2',
|
||||
'videotex-suppl',
|
||||
'PT2',
|
||||
'ES2',
|
||||
'MSZ_7795.3',
|
||||
'JIS_C6226-1983',
|
||||
'greek7',
|
||||
'ASMO_449',
|
||||
'iso-ir-90',
|
||||
'JIS_C6229-1984-a',
|
||||
'JIS_C6229-1984-b',
|
||||
'JIS_C6229-1984-b-add',
|
||||
'JIS_C6229-1984-hand',
|
||||
'JIS_C6229-1984-hand-add',
|
||||
'JIS_C6229-1984-kana',
|
||||
'ISO_2033-1983',
|
||||
'ANSI_X3.110-1983',
|
||||
'T.61-7bit',
|
||||
'T.61-8bit',
|
||||
'ECMA-cyrillic',
|
||||
'CSA_Z243.4-1985-1',
|
||||
'CSA_Z243.4-1985-2',
|
||||
'CSA_Z243.4-1985-gr',
|
||||
'ISO-8859-6-E',
|
||||
'ISO-8859-6-I',
|
||||
'T.101-G2',
|
||||
'ISO-8859-8-E',
|
||||
'ISO-8859-8-I',
|
||||
'CSN_369103',
|
||||
'JUS_I.B1.002',
|
||||
'IEC_P27-1',
|
||||
'JUS_I.B1.003-serb',
|
||||
'JUS_I.B1.003-mac',
|
||||
'greek-ccitt',
|
||||
'NC_NC00-10:81',
|
||||
'ISO_6937-2-25',
|
||||
'GOST_19768-74',
|
||||
'ISO_8859-supp',
|
||||
'ISO_10367-box',
|
||||
'latin-lap',
|
||||
'JIS_X0212-1990',
|
||||
'DS_2089',
|
||||
'us-dk',
|
||||
'dk-us',
|
||||
'KSC5636',
|
||||
'UNICODE-1-1-UTF-7',
|
||||
'ISO-2022-CN',
|
||||
'ISO-2022-CN-EXT',
|
||||
'UTF-8',
|
||||
'ISO-8859-13',
|
||||
'ISO-8859-14',
|
||||
'ISO-8859-15',
|
||||
'ISO-8859-16',
|
||||
'GBK',
|
||||
'GB18030',
|
||||
'OSD_EBCDIC_DF04_15',
|
||||
'OSD_EBCDIC_DF03_IRV',
|
||||
'OSD_EBCDIC_DF04_1',
|
||||
'ISO-11548-1',
|
||||
'KZ-1048',
|
||||
'ISO-10646-UCS-2',
|
||||
'ISO-10646-UCS-4',
|
||||
'ISO-10646-UCS-Basic',
|
||||
'ISO-10646-Unicode-Latin1',
|
||||
'ISO-10646-J-1',
|
||||
'ISO-Unicode-IBM-1261',
|
||||
'ISO-Unicode-IBM-1268',
|
||||
'ISO-Unicode-IBM-1276',
|
||||
'ISO-Unicode-IBM-1264',
|
||||
'ISO-Unicode-IBM-1265',
|
||||
'UNICODE-1-1',
|
||||
'SCSU',
|
||||
'UTF-7',
|
||||
'UTF-16BE',
|
||||
'UTF-16LE',
|
||||
'UTF-16',
|
||||
'CESU-8',
|
||||
'UTF-32',
|
||||
'UTF-32BE',
|
||||
'UTF-32LE',
|
||||
'BOCU-1',
|
||||
'ISO-8859-1-Windows-3.0-Latin-1',
|
||||
'ISO-8859-1-Windows-3.1-Latin-1',
|
||||
'ISO-8859-2-Windows-Latin-2',
|
||||
'ISO-8859-9-Windows-Latin-5',
|
||||
'hp-roman8',
|
||||
'Adobe-Standard-Encoding',
|
||||
'Ventura-US',
|
||||
'Ventura-International',
|
||||
'DEC-MCS',
|
||||
'IBM850',
|
||||
'PC8-Danish-Norwegian',
|
||||
'IBM862',
|
||||
'PC8-Turkish',
|
||||
'IBM-Symbols',
|
||||
'IBM-Thai',
|
||||
'HP-Legal',
|
||||
'HP-Pi-font',
|
||||
'HP-Math8',
|
||||
'Adobe-Symbol-Encoding',
|
||||
'HP-DeskTop',
|
||||
'Ventura-Math',
|
||||
'Microsoft-Publishing',
|
||||
'Windows-31J',
|
||||
'GB2312',
|
||||
'Big5',
|
||||
'macintosh',
|
||||
'IBM037',
|
||||
'IBM038',
|
||||
'IBM273',
|
||||
'IBM274',
|
||||
'IBM275',
|
||||
'IBM277',
|
||||
'IBM278',
|
||||
'IBM280',
|
||||
'IBM281',
|
||||
'IBM284',
|
||||
'IBM285',
|
||||
'IBM290',
|
||||
'IBM297',
|
||||
'IBM420',
|
||||
'IBM423',
|
||||
'IBM424',
|
||||
'IBM437',
|
||||
'IBM500',
|
||||
'IBM851',
|
||||
'IBM852',
|
||||
'IBM855',
|
||||
'IBM857',
|
||||
'IBM860',
|
||||
'IBM861',
|
||||
'IBM863',
|
||||
'IBM864',
|
||||
'IBM865',
|
||||
'IBM868',
|
||||
'IBM869',
|
||||
'IBM870',
|
||||
'IBM871',
|
||||
'IBM880',
|
||||
'IBM891',
|
||||
'IBM903',
|
||||
'IBM904',
|
||||
'IBM905',
|
||||
'IBM918',
|
||||
'IBM1026',
|
||||
'EBCDIC-AT-DE',
|
||||
'EBCDIC-AT-DE-A',
|
||||
'EBCDIC-CA-FR',
|
||||
'EBCDIC-DK-NO',
|
||||
'EBCDIC-DK-NO-A',
|
||||
'EBCDIC-FI-SE',
|
||||
'EBCDIC-FI-SE-A',
|
||||
'EBCDIC-FR',
|
||||
'EBCDIC-IT',
|
||||
'EBCDIC-PT',
|
||||
'EBCDIC-ES',
|
||||
'EBCDIC-ES-A',
|
||||
'EBCDIC-ES-S',
|
||||
'EBCDIC-UK',
|
||||
'EBCDIC-US',
|
||||
'UNKNOWN-8BIT',
|
||||
'MNEMONIC',
|
||||
'MNEM',
|
||||
'VISCII',
|
||||
'VIQR',
|
||||
'KOI8-R',
|
||||
'HZ-GB-2312',
|
||||
'IBM866',
|
||||
'IBM775',
|
||||
'KOI8-U',
|
||||
'IBM00858',
|
||||
'IBM00924',
|
||||
'IBM01140',
|
||||
'IBM01141',
|
||||
'IBM01142',
|
||||
'IBM01143',
|
||||
'IBM01144',
|
||||
'IBM01145',
|
||||
'IBM01146',
|
||||
'IBM01147',
|
||||
'IBM01148',
|
||||
'IBM01149',
|
||||
'Big5-HKSCS',
|
||||
'IBM1047',
|
||||
'PTCP154',
|
||||
'Amiga-1251',
|
||||
'KOI7-switched',
|
||||
'BRF',
|
||||
'TSCII',
|
||||
'CP51932',
|
||||
'windows-874',
|
||||
'windows-1250',
|
||||
'windows-1251',
|
||||
'windows-1252',
|
||||
'windows-1253',
|
||||
'windows-1254',
|
||||
'windows-1255',
|
||||
'windows-1256',
|
||||
'windows-1257',
|
||||
'windows-1258',
|
||||
'TIS-620',
|
||||
'CP50220'
|
||||
];
|
||||
|
||||
const CHARSET_MAP = new Map();
|
||||
|
||||
CHARACTER_SETS.forEach(entry => {
|
||||
let key = entry.replace(/[_-\s]/g, '').toLowerCase();
|
||||
let modifiedKey = key
|
||||
.replace(/^windows/, 'win')
|
||||
.replace(/^usascii/, 'ascii')
|
||||
.replace(/^iso8859/, 'latin');
|
||||
CHARSET_MAP.set(key, entry);
|
||||
if (!CHARSET_MAP.has(modifiedKey)) {
|
||||
CHARSET_MAP.set(modifiedKey, entry);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.resolveCharset = charset => {
|
||||
let key = charset.replace(/[_-\s]/g, '').toLowerCase();
|
||||
if (CHARSET_MAP.has(key)) {
|
||||
return CHARSET_MAP.get(key);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
146
backend/node_modules/imapflow/lib/commands/append.js
generated
vendored
Normal file
146
backend/node_modules/imapflow/lib/commands/append.js
generated
vendored
Normal file
@@ -0,0 +1,146 @@
|
||||
'use strict';
|
||||
|
||||
const { formatFlag, canUseFlag, formatDateTime, normalizePath, encodePath, comparePaths, enhanceCommandError } = require('../tools.js');
|
||||
|
||||
// Appends a message to a mailbox
|
||||
module.exports = async (connection, destination, content, flags, idate) => {
|
||||
if (![connection.states.AUTHENTICATED, connection.states.SELECTED].includes(connection.state) || !destination) {
|
||||
// nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof content === 'string') {
|
||||
content = Buffer.from(content);
|
||||
}
|
||||
|
||||
if (connection.capabilities.has('APPENDLIMIT')) {
|
||||
let appendLimit = connection.capabilities.get('APPENDLIMIT');
|
||||
if (typeof appendLimit === 'number' && appendLimit < content.length) {
|
||||
let err = new Error('Message content too big for APPENDLIMIT=' + appendLimit);
|
||||
err.serverResponseCode = 'APPENDLIMIT';
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
destination = normalizePath(connection, destination);
|
||||
|
||||
let expectExists = comparePaths(connection, connection.mailbox.path, destination);
|
||||
|
||||
flags = (Array.isArray(flags) ? flags : [].concat(flags || []))
|
||||
.map(flag => flag && formatFlag(flag.toString()))
|
||||
.filter(flag => flag && canUseFlag(connection.mailbox, flag));
|
||||
|
||||
let attributes = [{ type: 'ATOM', value: encodePath(connection, destination) }];
|
||||
|
||||
idate = idate ? formatDateTime(idate) : false;
|
||||
|
||||
if (flags.length || idate) {
|
||||
attributes.push(flags.map(flag => ({ type: 'ATOM', value: flag })));
|
||||
}
|
||||
|
||||
if (idate) {
|
||||
attributes.push({ type: 'STRING', value: idate }); // force quotes as required by date-time
|
||||
}
|
||||
|
||||
let isLiteral8 = false;
|
||||
if (connection.capabilities.has('BINARY') && !connection.disableBinary) {
|
||||
// Value is literal8 if it contains NULL bytes. The server must support the BINARY extension
|
||||
// and if it does not then send the value as a regular literal and hope for the best
|
||||
isLiteral8 = content.indexOf(Buffer.from([0])) >= 0;
|
||||
}
|
||||
|
||||
attributes.push({ type: 'LITERAL', value: content, isLiteral8 });
|
||||
|
||||
let map = { destination };
|
||||
if (connection.mailbox && connection.mailbox.path) {
|
||||
map.path = connection.mailbox.path;
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await connection.exec('APPEND', attributes, {
|
||||
untagged: expectExists
|
||||
? {
|
||||
EXISTS: async untagged => {
|
||||
map.seq = Number(untagged.command);
|
||||
|
||||
if (expectExists) {
|
||||
let prevCount = connection.mailbox.exists;
|
||||
if (map.seq !== prevCount) {
|
||||
connection.mailbox.exists = map.seq;
|
||||
connection.emit('exists', {
|
||||
path: connection.mailbox.path,
|
||||
count: map.seq,
|
||||
prevCount
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
: false
|
||||
});
|
||||
|
||||
let section = response.response.attributes && response.response.attributes[0] && response.response.attributes[0].section;
|
||||
if (section && section.length) {
|
||||
let responseCode = section[0] && typeof section[0].value === 'string' ? section[0].value : '';
|
||||
switch (responseCode.toUpperCase()) {
|
||||
case 'APPENDUID':
|
||||
{
|
||||
let uidValidity = section[1] && typeof section[1].value === 'string' && !isNaN(section[1].value) ? BigInt(section[1].value) : false;
|
||||
let uid = section[2] && typeof section[2].value === 'string' && !isNaN(section[2].value) ? Number(section[2].value) : false;
|
||||
if (uidValidity) {
|
||||
map.uidValidity = uidValidity;
|
||||
}
|
||||
if (uid) {
|
||||
map.uid = uid;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
response.next();
|
||||
|
||||
if (expectExists && !map.seq) {
|
||||
// try to use NOOP to get the new sequence number
|
||||
try {
|
||||
response = await connection.exec('NOOP', false, {
|
||||
untagged: {
|
||||
EXISTS: async untagged => {
|
||||
map.seq = Number(untagged.command);
|
||||
|
||||
if (expectExists) {
|
||||
let prevCount = connection.mailbox.exists;
|
||||
if (map.seq !== prevCount) {
|
||||
connection.mailbox.exists = map.seq;
|
||||
connection.emit('exists', {
|
||||
path: connection.mailbox.path,
|
||||
count: map.seq,
|
||||
prevCount
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
comment: 'Sequence not found from APPEND output'
|
||||
});
|
||||
response.next();
|
||||
} catch (err) {
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
}
|
||||
}
|
||||
|
||||
if (map.seq && !map.uid) {
|
||||
let list = await connection.search({ seq: map.seq }, { uid: true });
|
||||
if (list && list.length) {
|
||||
map.uid = list[0];
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
} catch (err) {
|
||||
await enhanceCommandError(err);
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
174
backend/node_modules/imapflow/lib/commands/authenticate.js
generated
vendored
Normal file
174
backend/node_modules/imapflow/lib/commands/authenticate.js
generated
vendored
Normal file
@@ -0,0 +1,174 @@
|
||||
'use strict';
|
||||
|
||||
const { getStatusCode, getErrorText } = require('../tools.js');
|
||||
|
||||
async function authOauth(connection, username, accessToken) {
|
||||
let oauthbearer;
|
||||
let command;
|
||||
let breaker;
|
||||
|
||||
if (connection.capabilities.has('AUTH=OAUTHBEARER')) {
|
||||
oauthbearer = [`n,a=${username},`, `host=${connection.servername}`, `port=993`, `auth=Bearer ${accessToken}`, '', ''].join('\x01');
|
||||
command = 'OAUTHBEARER';
|
||||
breaker = 'AQ==';
|
||||
} else if (connection.capabilities.has('AUTH=XOAUTH') || connection.capabilities.has('AUTH=XOAUTH2')) {
|
||||
oauthbearer = [`user=${username}`, `auth=Bearer ${accessToken}`, '', ''].join('\x01');
|
||||
command = 'XOAUTH2';
|
||||
breaker = '';
|
||||
}
|
||||
|
||||
let errorResponse = false;
|
||||
try {
|
||||
let response = await connection.exec(
|
||||
'AUTHENTICATE',
|
||||
[
|
||||
{ type: 'ATOM', value: command },
|
||||
{ type: 'ATOM', value: Buffer.from(oauthbearer).toString('base64'), sensitive: true }
|
||||
],
|
||||
{
|
||||
onPlusTag: async resp => {
|
||||
if (resp.attributes && resp.attributes[0] && resp.attributes[0].type === 'TEXT') {
|
||||
try {
|
||||
errorResponse = JSON.parse(Buffer.from(resp.attributes[0].value, 'base64').toString());
|
||||
} catch (err) {
|
||||
connection.log.debug({ errorResponse: resp.attributes[0].value, err });
|
||||
}
|
||||
}
|
||||
|
||||
connection.log.debug({ src: 'c', msg: breaker, comment: `Error response for ${command}` });
|
||||
connection.write(breaker);
|
||||
}
|
||||
}
|
||||
);
|
||||
response.next();
|
||||
|
||||
connection.authCapabilities.set(`AUTH=${command}`, true);
|
||||
|
||||
return username;
|
||||
} catch (err) {
|
||||
let errorCode = getStatusCode(err.response);
|
||||
if (errorCode) {
|
||||
err.serverResponseCode = errorCode;
|
||||
}
|
||||
err.authenticationFailed = true;
|
||||
err.response = await getErrorText(err.response);
|
||||
if (errorResponse) {
|
||||
err.oauthError = errorResponse;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function authLogin(connection, username, password) {
|
||||
let errorResponse = false;
|
||||
try {
|
||||
let response = await connection.exec('AUTHENTICATE', [{ type: 'ATOM', value: 'LOGIN' }], {
|
||||
onPlusTag: async resp => {
|
||||
if (resp.attributes && resp.attributes[0] && resp.attributes[0].type === 'TEXT') {
|
||||
let question = Buffer.from(resp.attributes[0].value, 'base64').toString();
|
||||
switch (
|
||||
question.toLowerCase().replace(/[:\x00]*$/, '') // eslint-disable-line no-control-regex
|
||||
) {
|
||||
case 'username':
|
||||
case 'user name': {
|
||||
let encodedUsername = Buffer.from(username).toString('base64');
|
||||
connection.log.debug({ src: 'c', msg: encodedUsername, comment: `Encoded username for AUTH=LOGIN` });
|
||||
connection.write(encodedUsername);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'password':
|
||||
connection.log.debug({ src: 'c', msg: '(* value hidden *)', comment: `Encoded password for AUTH=LOGIN` });
|
||||
connection.write(Buffer.from(password).toString('base64'));
|
||||
break;
|
||||
|
||||
default: {
|
||||
let error = new Error(`Unknown LOGIN question "${question}"`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
response.next();
|
||||
|
||||
connection.authCapabilities.set(`AUTH=LOGIN`, true);
|
||||
|
||||
return username;
|
||||
} catch (err) {
|
||||
let errorCode = getStatusCode(err.response);
|
||||
if (errorCode) {
|
||||
err.serverResponseCode = errorCode;
|
||||
}
|
||||
err.authenticationFailed = true;
|
||||
err.response = await getErrorText(err.response);
|
||||
if (errorResponse) {
|
||||
err.oauthError = errorResponse;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function authPlain(connection, username, password, authzid) {
|
||||
let errorResponse = false;
|
||||
try {
|
||||
let response = await connection.exec('AUTHENTICATE', [{ type: 'ATOM', value: 'PLAIN' }], {
|
||||
onPlusTag: async () => {
|
||||
// SASL PLAIN format: [authzid]\x00authcid\x00password
|
||||
// authzid: authorization identity (who to impersonate)
|
||||
// authcid: authentication identity (who is authenticating)
|
||||
let authzidValue = authzid || '';
|
||||
let encodedResponse = Buffer.from([authzidValue, username, password].join('\x00')).toString('base64');
|
||||
let loggedResponse = Buffer.from([authzidValue, username, '(* value hidden *)'].join('\x00')).toString('base64');
|
||||
connection.log.debug({ src: 'c', msg: loggedResponse, comment: `Encoded response for AUTH=PLAIN${authzid ? ' with authzid' : ''}` });
|
||||
connection.write(encodedResponse);
|
||||
}
|
||||
});
|
||||
|
||||
response.next();
|
||||
|
||||
connection.authCapabilities.set(`AUTH=PLAIN`, true);
|
||||
|
||||
// Return the identity we're authorized as (authzid if provided, otherwise username)
|
||||
return authzid || username;
|
||||
} catch (err) {
|
||||
let errorCode = getStatusCode(err.response);
|
||||
if (errorCode) {
|
||||
err.serverResponseCode = errorCode;
|
||||
}
|
||||
err.authenticationFailed = true;
|
||||
err.response = await getErrorText(err.response);
|
||||
if (errorResponse) {
|
||||
err.oauthError = errorResponse;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// Authenticates user using LOGIN
|
||||
module.exports = async (connection, username, { accessToken, password, loginMethod, authzid }) => {
|
||||
if (connection.state !== connection.states.NOT_AUTHENTICATED) {
|
||||
// nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
if (accessToken) {
|
||||
// AUTH=OAUTHBEARER and AUTH=XOAUTH in the context of OAuth2 or very similar so we can handle these together
|
||||
if (connection.capabilities.has('AUTH=OAUTHBEARER') || connection.capabilities.has('AUTH=XOAUTH') || connection.capabilities.has('AUTH=XOAUTH2')) {
|
||||
return await authOauth(connection, username, accessToken);
|
||||
}
|
||||
}
|
||||
|
||||
if (password) {
|
||||
if ((!loginMethod && connection.capabilities.has('AUTH=PLAIN')) || loginMethod === 'AUTH=PLAIN') {
|
||||
return await authPlain(connection, username, password, authzid);
|
||||
}
|
||||
|
||||
if ((!loginMethod && connection.capabilities.has('AUTH=LOGIN')) || loginMethod === 'AUTH=LOGIN') {
|
||||
return await authLogin(connection, username, password);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Unsupported authentication mechanism');
|
||||
};
|
||||
20
backend/node_modules/imapflow/lib/commands/capability.js
generated
vendored
Normal file
20
backend/node_modules/imapflow/lib/commands/capability.js
generated
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
'use strict';
|
||||
|
||||
// Refresh capabilities from server
|
||||
module.exports = async connection => {
|
||||
if (connection.capabilities.size && !connection.expectCapabilityUpdate) {
|
||||
return connection.capabilities;
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
// untagged capability response is processed by global handler
|
||||
response = await connection.exec('CAPABILITY');
|
||||
|
||||
response.next();
|
||||
return connection.capabilities;
|
||||
} catch (err) {
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
return false;
|
||||
}
|
||||
};
|
||||
28
backend/node_modules/imapflow/lib/commands/close.js
generated
vendored
Normal file
28
backend/node_modules/imapflow/lib/commands/close.js
generated
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
'use strict';
|
||||
|
||||
// Closes a mailbox
|
||||
module.exports = async connection => {
|
||||
if (connection.state !== connection.states.SELECTED) {
|
||||
// nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await connection.exec('CLOSE');
|
||||
response.next();
|
||||
|
||||
let currentMailbox = connection.mailbox;
|
||||
connection.mailbox = false;
|
||||
connection.currentSelectCommand = false;
|
||||
connection.state = connection.states.AUTHENTICATED;
|
||||
|
||||
if (currentMailbox) {
|
||||
connection.emit('mailboxClose', currentMailbox);
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
return false;
|
||||
}
|
||||
};
|
||||
19
backend/node_modules/imapflow/lib/commands/compress.js
generated
vendored
Normal file
19
backend/node_modules/imapflow/lib/commands/compress.js
generated
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
'use strict';
|
||||
|
||||
// Requests compression from server
|
||||
module.exports = async connection => {
|
||||
if (!connection.capabilities.has('COMPRESS=DEFLATE') || connection._inflate) {
|
||||
// nothing to do here
|
||||
return false;
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await connection.exec('COMPRESS', [{ type: 'ATOM', value: 'DEFLATE' }]);
|
||||
response.next();
|
||||
return true;
|
||||
} catch (err) {
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
return false;
|
||||
}
|
||||
};
|
||||
51
backend/node_modules/imapflow/lib/commands/copy.js
generated
vendored
Normal file
51
backend/node_modules/imapflow/lib/commands/copy.js
generated
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
'use strict';
|
||||
|
||||
const { normalizePath, encodePath, expandRange, enhanceCommandError } = require('../tools.js');
|
||||
|
||||
// Copies messages from current mailbox to some other mailbox
|
||||
module.exports = async (connection, range, destination, options) => {
|
||||
if (connection.state !== connection.states.SELECTED || !range || !destination) {
|
||||
// nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
destination = normalizePath(connection, destination);
|
||||
|
||||
let attributes = [
|
||||
{ type: 'SEQUENCE', value: range },
|
||||
{ type: 'ATOM', value: encodePath(connection, destination) }
|
||||
];
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await connection.exec(options.uid ? 'UID COPY' : 'COPY', attributes);
|
||||
response.next();
|
||||
|
||||
let map = { path: connection.mailbox.path, destination };
|
||||
let section = response.response.attributes && response.response.attributes[0] && response.response.attributes[0].section;
|
||||
let responseCode = section && section.length && section[0] && typeof section[0].value === 'string' ? section[0].value : '';
|
||||
switch (responseCode) {
|
||||
case 'COPYUID':
|
||||
{
|
||||
let uidValidity = section[1] && typeof section[1].value === 'string' && !isNaN(section[1].value) ? BigInt(section[1].value) : false;
|
||||
if (uidValidity) {
|
||||
map.uidValidity = uidValidity;
|
||||
}
|
||||
|
||||
let sourceUids = section[2] && typeof section[2].value === 'string' ? expandRange(section[2].value) : false;
|
||||
let destinationUids = section[3] && typeof section[3].value === 'string' ? expandRange(section[3].value) : false;
|
||||
if (sourceUids && destinationUids && sourceUids.length === destinationUids.length) {
|
||||
map.uidMap = new Map(sourceUids.map((uid, i) => [uid, destinationUids[i]]));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return map;
|
||||
} catch (err) {
|
||||
await enhanceCommandError(err);
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
return false;
|
||||
}
|
||||
};
|
||||
76
backend/node_modules/imapflow/lib/commands/create.js
generated
vendored
Normal file
76
backend/node_modules/imapflow/lib/commands/create.js
generated
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
'use strict';
|
||||
|
||||
const { encodePath, normalizePath, getStatusCode, enhanceCommandError } = require('../tools.js');
|
||||
|
||||
// Creates a new mailbox
|
||||
module.exports = async (connection, path) => {
|
||||
if (![connection.states.AUTHENTICATED, connection.states.SELECTED].includes(connection.state)) {
|
||||
// nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
path = normalizePath(connection, path);
|
||||
|
||||
let response;
|
||||
try {
|
||||
let map = {
|
||||
path
|
||||
};
|
||||
response = await connection.exec('CREATE', [{ type: 'ATOM', value: encodePath(connection, path) }]);
|
||||
|
||||
let section =
|
||||
response.response.attributes &&
|
||||
response.response.attributes[0] &&
|
||||
response.response.attributes[0].section &&
|
||||
response.response.attributes[0].section.length
|
||||
? response.response.attributes[0].section
|
||||
: false;
|
||||
|
||||
if (section) {
|
||||
let key;
|
||||
section.forEach((attribute, i) => {
|
||||
if (i % 2 === 0) {
|
||||
key = attribute && typeof attribute.value === 'string' ? attribute.value : false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
|
||||
let value;
|
||||
switch (key.toLowerCase()) {
|
||||
case 'mailboxid':
|
||||
key = 'mailboxId';
|
||||
value = Array.isArray(attribute) && attribute[0] && typeof attribute[0].value === 'string' ? attribute[0].value : false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (key && value) {
|
||||
map[key] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
map.created = true;
|
||||
response.next();
|
||||
|
||||
//make sure we are subscribed to the new folder as well
|
||||
await connection.run('SUBSCRIBE', path);
|
||||
|
||||
return map;
|
||||
} catch (err) {
|
||||
let errorCode = getStatusCode(err.response);
|
||||
if (errorCode === 'ALREADYEXISTS') {
|
||||
// no need to do anything, mailbox already exists
|
||||
return {
|
||||
path,
|
||||
created: false
|
||||
};
|
||||
}
|
||||
|
||||
await enhanceCommandError(err);
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
31
backend/node_modules/imapflow/lib/commands/delete.js
generated
vendored
Normal file
31
backend/node_modules/imapflow/lib/commands/delete.js
generated
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
'use strict';
|
||||
|
||||
const { encodePath, normalizePath, enhanceCommandError } = require('../tools.js');
|
||||
|
||||
// Deletes an existing mailbox
|
||||
module.exports = async (connection, path) => {
|
||||
if (![connection.states.AUTHENTICATED, connection.states.SELECTED].includes(connection.state)) {
|
||||
// nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
path = normalizePath(connection, path);
|
||||
|
||||
if (connection.state === connection.states.SELECTED && connection.mailbox.path === path) {
|
||||
await connection.run('CLOSE');
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
let map = {
|
||||
path
|
||||
};
|
||||
response = await connection.exec('DELETE', [{ type: 'ATOM', value: encodePath(connection, path) }]);
|
||||
response.next();
|
||||
return map;
|
||||
} catch (err) {
|
||||
await enhanceCommandError(err);
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
43
backend/node_modules/imapflow/lib/commands/enable.js
generated
vendored
Normal file
43
backend/node_modules/imapflow/lib/commands/enable.js
generated
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
'use strict';
|
||||
|
||||
// Enables extensions
|
||||
module.exports = async (connection, extensionList) => {
|
||||
if (!connection.capabilities.has('ENABLE') || connection.state !== connection.states.AUTHENTICATED) {
|
||||
// nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
extensionList = extensionList.filter(extension => connection.capabilities.has(extension.toUpperCase()));
|
||||
if (!extensionList.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
let enabled = new Set();
|
||||
response = await connection.exec(
|
||||
'ENABLE',
|
||||
extensionList.map(extension => ({ type: 'ATOM', value: extension.toUpperCase() })),
|
||||
{
|
||||
untagged: {
|
||||
ENABLED: async untagged => {
|
||||
if (!untagged.attributes || !untagged.attributes.length) {
|
||||
return;
|
||||
}
|
||||
untagged.attributes.forEach(attr => {
|
||||
if (attr.value && typeof attr.value === 'string') {
|
||||
enabled.add(attr.value.toUpperCase().trim());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
connection.enabled = enabled;
|
||||
response.next();
|
||||
return enabled;
|
||||
} catch (err) {
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
return false;
|
||||
}
|
||||
};
|
||||
41
backend/node_modules/imapflow/lib/commands/expunge.js
generated
vendored
Normal file
41
backend/node_modules/imapflow/lib/commands/expunge.js
generated
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
'use strict';
|
||||
|
||||
const { enhanceCommandError } = require('../tools.js');
|
||||
|
||||
// Deletes specified messages
|
||||
module.exports = async (connection, range, options) => {
|
||||
if (connection.state !== connection.states.SELECTED || !range) {
|
||||
// nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
|
||||
await connection.messageFlagsAdd(range, ['\\Deleted'], options);
|
||||
|
||||
let byUid = options.uid && connection.capabilities.has('UIDPLUS');
|
||||
let command = byUid ? 'UID EXPUNGE' : 'EXPUNGE';
|
||||
let attributes = byUid ? [{ type: 'SEQUENCE', value: range }] : false;
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await connection.exec(command, attributes);
|
||||
|
||||
// A OK [HIGHESTMODSEQ 9122] Expunge completed (0.010 + 0.000 + 0.012 secs).
|
||||
let section = response.response.attributes && response.response.attributes[0] && response.response.attributes[0].section;
|
||||
let responseCode = section && section.length && section[0] && typeof section[0].value === 'string' ? section[0].value : '';
|
||||
if (responseCode.toUpperCase() === 'HIGHESTMODSEQ') {
|
||||
let highestModseq = section[1] && typeof section[1].value === 'string' && !isNaN(section[1].value) ? BigInt(section[1].value) : false;
|
||||
if (highestModseq && (!connection.mailbox.highestModseq || highestModseq > connection.mailbox.highestModseq)) {
|
||||
connection.mailbox.highestModseq = highestModseq;
|
||||
}
|
||||
}
|
||||
|
||||
response.next();
|
||||
return true;
|
||||
} catch (err) {
|
||||
await enhanceCommandError(err);
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
return false;
|
||||
}
|
||||
};
|
||||
222
backend/node_modules/imapflow/lib/commands/fetch.js
generated
vendored
Normal file
222
backend/node_modules/imapflow/lib/commands/fetch.js
generated
vendored
Normal file
@@ -0,0 +1,222 @@
|
||||
'use strict';
|
||||
|
||||
const { formatMessageResponse } = require('../tools');
|
||||
|
||||
// Fetches emails from server
|
||||
module.exports = async (connection, range, query, options) => {
|
||||
if (connection.state !== connection.states.SELECTED || !range) {
|
||||
// nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
|
||||
let mailbox = connection.mailbox;
|
||||
|
||||
const commandKey = connection.capabilities.has('BINARY') && options.binary && !connection.disableBinary ? 'BINARY' : 'BODY';
|
||||
|
||||
let retryCount = 0;
|
||||
const maxRetries = 4;
|
||||
const baseDelay = 1000; // Start with 1 second delay
|
||||
|
||||
while (retryCount < maxRetries) {
|
||||
let messages = {
|
||||
count: 0,
|
||||
list: []
|
||||
};
|
||||
|
||||
let response;
|
||||
try {
|
||||
let attributes = [{ type: 'SEQUENCE', value: (range || '*').toString() }];
|
||||
|
||||
let queryStructure = [];
|
||||
|
||||
let setBodyPeek = (attributes, partial) => {
|
||||
let bodyPeek = {
|
||||
type: 'ATOM',
|
||||
value: `${commandKey}.PEEK`,
|
||||
section: [],
|
||||
partial
|
||||
};
|
||||
|
||||
if (Array.isArray(attributes)) {
|
||||
attributes.forEach(attribute => {
|
||||
bodyPeek.section.push(attribute);
|
||||
});
|
||||
} else if (attributes) {
|
||||
bodyPeek.section.push(attributes);
|
||||
}
|
||||
|
||||
queryStructure.push(bodyPeek);
|
||||
};
|
||||
|
||||
['all', 'fast', 'full', 'uid', 'flags', 'bodyStructure', 'envelope', 'internalDate'].forEach(key => {
|
||||
if (query[key]) {
|
||||
queryStructure.push({ type: 'ATOM', value: key.toUpperCase() });
|
||||
}
|
||||
});
|
||||
|
||||
if (query.size) {
|
||||
queryStructure.push({ type: 'ATOM', value: 'RFC822.SIZE' });
|
||||
}
|
||||
|
||||
if (query.source) {
|
||||
let partial;
|
||||
if (typeof query.source === 'object' && (query.source.start || query.source.maxLength)) {
|
||||
partial = [Number(query.source.start) || 0];
|
||||
if (query.source.maxLength && !isNaN(query.source.maxLength)) {
|
||||
partial.push(Number(query.source.maxLength));
|
||||
}
|
||||
}
|
||||
queryStructure.push({ type: 'ATOM', value: `${commandKey}.PEEK`, section: [], partial });
|
||||
}
|
||||
|
||||
// if possible, always request for unique email id
|
||||
if (connection.capabilities.has('OBJECTID')) {
|
||||
queryStructure.push({ type: 'ATOM', value: 'EMAILID' });
|
||||
} else if (connection.capabilities.has('X-GM-EXT-1')) {
|
||||
queryStructure.push({ type: 'ATOM', value: 'X-GM-MSGID' });
|
||||
}
|
||||
|
||||
if (query.threadId) {
|
||||
if (connection.capabilities.has('OBJECTID')) {
|
||||
queryStructure.push({ type: 'ATOM', value: 'THREADID' });
|
||||
} else if (connection.capabilities.has('X-GM-EXT-1')) {
|
||||
queryStructure.push({ type: 'ATOM', value: 'X-GM-THRID' });
|
||||
}
|
||||
}
|
||||
|
||||
if (query.labels) {
|
||||
if (connection.capabilities.has('X-GM-EXT-1')) {
|
||||
queryStructure.push({ type: 'ATOM', value: 'X-GM-LABELS' });
|
||||
}
|
||||
}
|
||||
|
||||
// always ask for modseq if possible
|
||||
if (connection.enabled.has('CONDSTORE') && !mailbox.noModseq) {
|
||||
queryStructure.push({ type: 'ATOM', value: 'MODSEQ' });
|
||||
}
|
||||
|
||||
// always make sure to include UID in the request as well even though server might auto-add it itself
|
||||
if (!query.uid) {
|
||||
queryStructure.push({ type: 'ATOM', value: 'UID' });
|
||||
}
|
||||
|
||||
if (query.headers) {
|
||||
if (Array.isArray(query.headers)) {
|
||||
setBodyPeek([{ type: 'ATOM', value: 'HEADER.FIELDS' }, query.headers.map(header => ({ type: 'ATOM', value: header }))]);
|
||||
} else {
|
||||
setBodyPeek({ type: 'ATOM', value: 'HEADER' });
|
||||
}
|
||||
}
|
||||
|
||||
if (query.bodyParts && query.bodyParts.length) {
|
||||
query.bodyParts.forEach(part => {
|
||||
if (!part) {
|
||||
return;
|
||||
}
|
||||
let key;
|
||||
let partial;
|
||||
if (typeof part === 'object') {
|
||||
if (!part.key || typeof part.key !== 'string') {
|
||||
return;
|
||||
}
|
||||
key = part.key.toUpperCase();
|
||||
if (part.start || part.maxLength) {
|
||||
partial = [Number(part.start) || 0];
|
||||
if (part.maxLength && !isNaN(part.maxLength)) {
|
||||
partial.push(Number(part.maxLength));
|
||||
}
|
||||
}
|
||||
} else if (typeof part === 'string') {
|
||||
key = part.toUpperCase();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
setBodyPeek({ type: 'ATOM', value: key }, partial);
|
||||
});
|
||||
}
|
||||
|
||||
if (queryStructure.length === 1) {
|
||||
queryStructure = queryStructure.pop();
|
||||
}
|
||||
|
||||
attributes.push(queryStructure);
|
||||
|
||||
if (options.changedSince && connection.enabled.has('CONDSTORE') && !mailbox.noModseq) {
|
||||
let changedSinceArgs = [
|
||||
{
|
||||
type: 'ATOM',
|
||||
value: 'CHANGEDSINCE'
|
||||
},
|
||||
{
|
||||
type: 'ATOM',
|
||||
value: options.changedSince.toString()
|
||||
}
|
||||
];
|
||||
|
||||
if (options.uid && connection.enabled.has('QRESYNC')) {
|
||||
changedSinceArgs.push({
|
||||
type: 'ATOM',
|
||||
value: 'VANISHED'
|
||||
});
|
||||
}
|
||||
|
||||
attributes.push(changedSinceArgs);
|
||||
}
|
||||
|
||||
response = await connection.exec(options.uid ? 'UID FETCH' : 'FETCH', attributes, {
|
||||
untagged: {
|
||||
FETCH: async untagged => {
|
||||
messages.count++;
|
||||
let formatted = await formatMessageResponse(untagged, mailbox);
|
||||
if (typeof options.onUntaggedFetch === 'function') {
|
||||
await new Promise((resolve, reject) => {
|
||||
options.onUntaggedFetch(formatted, err => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
messages.list.push(formatted);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
response.next();
|
||||
return messages;
|
||||
} catch (err) {
|
||||
if (err.code === 'ETHROTTLE') {
|
||||
// Calculate exponential backoff delay
|
||||
const backoffDelay = Math.min(baseDelay * Math.pow(2, retryCount), 30000); // Cap at 30 seconds
|
||||
|
||||
// Use throttle reset time if provided and longer than backoff
|
||||
const delay = err.throttleReset && err.throttleReset > backoffDelay ? err.throttleReset : backoffDelay;
|
||||
|
||||
connection.log.warn({
|
||||
msg: 'Retrying throttled request with exponential backoff',
|
||||
cid: connection.id,
|
||||
code: err.code,
|
||||
response: err.responseText,
|
||||
throttleReset: err.throttleReset,
|
||||
retryCount,
|
||||
delayMs: delay
|
||||
});
|
||||
|
||||
// Wait before retrying
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
|
||||
retryCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
};
|
||||
62
backend/node_modules/imapflow/lib/commands/id.js
generated
vendored
Normal file
62
backend/node_modules/imapflow/lib/commands/id.js
generated
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
'use strict';
|
||||
|
||||
const { formatDateTime } = require('../tools.js');
|
||||
|
||||
// Sends ID info to server and updates server info data based on response
|
||||
module.exports = async (connection, clientInfo) => {
|
||||
if (!connection.capabilities.has('ID')) {
|
||||
// nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
let map = {};
|
||||
|
||||
// convert object into an array of value tuples
|
||||
let formattedClientInfo = !clientInfo
|
||||
? null
|
||||
: Object.keys(clientInfo)
|
||||
.map(key => [key, formatValue(key, clientInfo[key])])
|
||||
.filter(entry => entry[1])
|
||||
.flatMap(entry => entry);
|
||||
|
||||
if (formattedClientInfo && !formattedClientInfo.length) {
|
||||
// value array has no elements
|
||||
formattedClientInfo = null;
|
||||
}
|
||||
|
||||
response = await connection.exec('ID', [formattedClientInfo], {
|
||||
untagged: {
|
||||
ID: async untagged => {
|
||||
let params = untagged.attributes && untagged.attributes[0];
|
||||
let key;
|
||||
(Array.isArray(params) ? params : [].concat(params || [])).forEach((val, i) => {
|
||||
if (i % 2 === 0) {
|
||||
key = val.value;
|
||||
} else if (typeof key === 'string' && typeof val.value === 'string') {
|
||||
map[key.toLowerCase().trim()] = val.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
connection.serverInfo = map;
|
||||
response.next();
|
||||
return map;
|
||||
} catch (err) {
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
function formatValue(key, value) {
|
||||
switch (key.toLowerCase()) {
|
||||
case 'date':
|
||||
// Date has to be in imap date-time format
|
||||
return formatDateTime(value);
|
||||
default:
|
||||
// Other values are strings without newlines
|
||||
return (value || '').toString().replace(/\s+/g, ' ');
|
||||
}
|
||||
}
|
||||
211
backend/node_modules/imapflow/lib/commands/idle.js
generated
vendored
Normal file
211
backend/node_modules/imapflow/lib/commands/idle.js
generated
vendored
Normal file
@@ -0,0 +1,211 @@
|
||||
'use strict';
|
||||
|
||||
const NOOP_INTERVAL = 2 * 60 * 1000;
|
||||
|
||||
async function runIdle(connection) {
|
||||
let response;
|
||||
|
||||
let preCheckWaitQueue = [];
|
||||
try {
|
||||
connection.idling = true;
|
||||
|
||||
//let idleSent = false;
|
||||
let doneRequested = false;
|
||||
let doneSent = false;
|
||||
let canEnd = false;
|
||||
|
||||
let preCheck = async () => {
|
||||
doneRequested = true;
|
||||
if (canEnd && !doneSent) {
|
||||
connection.log.debug({
|
||||
src: 'c',
|
||||
msg: `DONE`,
|
||||
comment: `breaking IDLE`,
|
||||
lockId: connection.currentLock?.lockId,
|
||||
path: connection.mailbox && connection.mailbox.path
|
||||
});
|
||||
connection.write('DONE');
|
||||
doneSent = true;
|
||||
|
||||
connection.idling = false;
|
||||
connection.preCheck = false; // unset itself
|
||||
|
||||
while (preCheckWaitQueue.length) {
|
||||
let { resolve } = preCheckWaitQueue.shift();
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let connectionPreCheck = () => {
|
||||
let handler = new Promise((resolve, reject) => {
|
||||
preCheckWaitQueue.push({ resolve, reject });
|
||||
});
|
||||
|
||||
connection.log.trace({
|
||||
msg: 'Requesting IDLE break',
|
||||
lockId: connection.currentLock?.lockId,
|
||||
path: connection.mailbox && connection.mailbox.path,
|
||||
queued: preCheckWaitQueue.length,
|
||||
doneRequested,
|
||||
canEnd,
|
||||
doneSent
|
||||
});
|
||||
|
||||
preCheck().catch(err => connection.log.warn({ err, cid: connection.id }));
|
||||
|
||||
return handler;
|
||||
};
|
||||
|
||||
connection.preCheck = connectionPreCheck;
|
||||
|
||||
response = await connection.exec('IDLE', false, {
|
||||
onPlusTag: async () => {
|
||||
connection.log.debug({ msg: `Initiated IDLE, waiting for server input`, lockId: connection.currentLock?.lockId, doneRequested });
|
||||
canEnd = true;
|
||||
if (doneRequested) {
|
||||
try {
|
||||
await preCheck();
|
||||
} catch (err) {
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
}
|
||||
}
|
||||
},
|
||||
onSend: () => {
|
||||
//idleSent = true;
|
||||
}
|
||||
});
|
||||
|
||||
// unset before response.next() if preCheck function is not already cleared (usually is)
|
||||
if (typeof connection.preCheck === 'function' && connection.preCheck === connectionPreCheck) {
|
||||
connection.log.trace({
|
||||
msg: 'Clearing pre-check function',
|
||||
lockId: connection.currentLock?.lockId,
|
||||
path: connection.mailbox && connection.mailbox.path,
|
||||
queued: preCheckWaitQueue.length,
|
||||
doneRequested,
|
||||
canEnd,
|
||||
doneSent
|
||||
});
|
||||
connection.preCheck = false;
|
||||
while (preCheckWaitQueue.length) {
|
||||
let { resolve } = preCheckWaitQueue.shift();
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
|
||||
response.next();
|
||||
return;
|
||||
} catch (err) {
|
||||
connection.preCheck = false;
|
||||
connection.idling = false;
|
||||
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
while (preCheckWaitQueue.length) {
|
||||
let { reject } = preCheckWaitQueue.shift();
|
||||
reject(err);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Listens for changes in mailbox
|
||||
module.exports = async (connection, maxIdleTime) => {
|
||||
if (connection.state !== connection.states.SELECTED) {
|
||||
// nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
if (connection.capabilities.has('IDLE')) {
|
||||
let idleTimer;
|
||||
let stillIdling = false;
|
||||
let runIdleLoop = async () => {
|
||||
if (maxIdleTime) {
|
||||
idleTimer = setTimeout(() => {
|
||||
if (connection.idling) {
|
||||
if (typeof connection.preCheck === 'function') {
|
||||
stillIdling = true;
|
||||
// request IDLE break if IDLE has been running for allowed time
|
||||
connection.log.trace({ msg: 'Max allowed IDLE time reached', cid: connection.id });
|
||||
connection.preCheck().catch(err => connection.log.warn({ err, cid: connection.id }));
|
||||
}
|
||||
}
|
||||
}, maxIdleTime);
|
||||
}
|
||||
let resp = await runIdle(connection);
|
||||
clearTimeout(idleTimer);
|
||||
if (stillIdling) {
|
||||
stillIdling = false;
|
||||
return runIdleLoop();
|
||||
}
|
||||
return resp;
|
||||
};
|
||||
return runIdleLoop();
|
||||
}
|
||||
|
||||
let idleTimer;
|
||||
return new Promise(resolve => {
|
||||
if (!connection.currentSelectCommand) {
|
||||
return resolve();
|
||||
}
|
||||
|
||||
// no IDLE support, fallback to NOOP'ing
|
||||
connection.preCheck = async () => {
|
||||
connection.preCheck = false; // unset itself
|
||||
clearTimeout(idleTimer);
|
||||
connection.log.debug({ src: 'c', msg: `breaking NOOP loop` });
|
||||
connection.idling = false;
|
||||
resolve();
|
||||
};
|
||||
|
||||
let selectCommand = connection.currentSelectCommand;
|
||||
|
||||
let idleCheck = async () => {
|
||||
let response;
|
||||
switch (connection.missingIdleCommand) {
|
||||
case 'SELECT':
|
||||
// FIXME: somehow a loop occurs after some time of idling with SELECT
|
||||
connection.log.debug({ src: 'c', msg: `Running SELECT to detect changes in folder` });
|
||||
response = await connection.exec(selectCommand.command, selectCommand.arguments);
|
||||
break;
|
||||
|
||||
case 'STATUS':
|
||||
{
|
||||
let statusArgs = [selectCommand.arguments[0], []]; // path
|
||||
for (let key of ['MESSAGES', 'UIDNEXT', 'UIDVALIDITY', 'UNSEEN']) {
|
||||
statusArgs[1].push({ type: 'ATOM', value: key.toUpperCase() });
|
||||
}
|
||||
connection.log.debug({ src: 'c', msg: `Running STATUS to detect changes in folder` });
|
||||
response = await connection.exec('STATUS', statusArgs);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'NOOP':
|
||||
default:
|
||||
response = await connection.exec('NOOP', false, { comment: 'IDLE not supported' });
|
||||
break;
|
||||
}
|
||||
response.next();
|
||||
};
|
||||
|
||||
let noopInterval = maxIdleTime ? Math.min(NOOP_INTERVAL, maxIdleTime) : NOOP_INTERVAL;
|
||||
|
||||
let runLoop = () => {
|
||||
idleCheck()
|
||||
.then(() => {
|
||||
clearTimeout(idleTimer);
|
||||
idleTimer = setTimeout(runLoop, noopInterval);
|
||||
})
|
||||
.catch(err => {
|
||||
clearTimeout(idleTimer);
|
||||
connection.preCheck = false;
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
resolve();
|
||||
});
|
||||
};
|
||||
|
||||
connection.log.debug({ src: 'c', msg: `initiated NOOP loop` });
|
||||
connection.idling = true;
|
||||
runLoop();
|
||||
});
|
||||
};
|
||||
328
backend/node_modules/imapflow/lib/commands/list.js
generated
vendored
Normal file
328
backend/node_modules/imapflow/lib/commands/list.js
generated
vendored
Normal file
@@ -0,0 +1,328 @@
|
||||
'use strict';
|
||||
|
||||
const { decodePath, encodePath, normalizePath } = require('../tools.js');
|
||||
const { specialUse } = require('../special-use');
|
||||
|
||||
// Lists mailboxes from server
|
||||
module.exports = async (connection, reference, mailbox, options) => {
|
||||
options = options || {};
|
||||
|
||||
const FLAG_SORT_ORDER = ['\\Inbox', '\\Flagged', '\\Sent', '\\Drafts', '\\All', '\\Archive', '\\Junk', '\\Trash'];
|
||||
const SOURCE_SORT_ORDER = ['user', 'extension', 'name'];
|
||||
|
||||
let listCommand = connection.capabilities.has('XLIST') && !connection.capabilities.has('SPECIAL-USE') ? 'XLIST' : 'LIST';
|
||||
|
||||
let response;
|
||||
try {
|
||||
let entries = [];
|
||||
|
||||
let statusMap = new Map();
|
||||
let returnArgs = [];
|
||||
let statusQueryAttributes = [];
|
||||
|
||||
if (options.statusQuery) {
|
||||
Object.keys(options.statusQuery || {}).forEach(key => {
|
||||
if (!options.statusQuery[key]) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (key.toUpperCase()) {
|
||||
case 'MESSAGES':
|
||||
case 'RECENT':
|
||||
case 'UIDNEXT':
|
||||
case 'UIDVALIDITY':
|
||||
case 'UNSEEN':
|
||||
statusQueryAttributes.push({ type: 'ATOM', value: key.toUpperCase() });
|
||||
break;
|
||||
|
||||
case 'HIGHESTMODSEQ':
|
||||
if (connection.capabilities.has('CONDSTORE')) {
|
||||
statusQueryAttributes.push({ type: 'ATOM', value: key.toUpperCase() });
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (listCommand === 'LIST' && connection.capabilities.has('LIST-STATUS') && statusQueryAttributes.length) {
|
||||
returnArgs.push({ type: 'ATOM', value: 'STATUS' }, statusQueryAttributes);
|
||||
if (connection.capabilities.has('SPECIAL-USE')) {
|
||||
returnArgs.push({ type: 'ATOM', value: 'SPECIAL-USE' });
|
||||
}
|
||||
}
|
||||
|
||||
let specialUseMatches = {};
|
||||
let addSpecialUseMatch = (entry, type, source) => {
|
||||
if (!specialUseMatches[type]) {
|
||||
specialUseMatches[type] = [];
|
||||
}
|
||||
specialUseMatches[type].push({ entry, source });
|
||||
};
|
||||
|
||||
let specialUseHints = {};
|
||||
if (options.specialUseHints && typeof options.specialUseHints === 'object') {
|
||||
for (let type of Object.keys(options.specialUseHints)) {
|
||||
if (
|
||||
['sent', 'junk', 'trash', 'drafts', 'archive'].includes(type) &&
|
||||
options.specialUseHints[type] &&
|
||||
typeof options.specialUseHints[type] === 'string'
|
||||
) {
|
||||
specialUseHints[normalizePath(connection, options.specialUseHints[type])] = `\\${type.replace(/^./, c => c.toUpperCase())}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let runList = async (reference, mailbox) => {
|
||||
const cmdArgs = [encodePath(connection, reference), encodePath(connection, mailbox)];
|
||||
|
||||
if (returnArgs.length) {
|
||||
cmdArgs.push({ type: 'ATOM', value: 'RETURN' }, returnArgs);
|
||||
}
|
||||
|
||||
response = await connection.exec(listCommand, cmdArgs, {
|
||||
untagged: {
|
||||
[listCommand]: async untagged => {
|
||||
if (!untagged.attributes || !untagged.attributes.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let entry = {
|
||||
path: normalizePath(connection, decodePath(connection, (untagged.attributes[2] && untagged.attributes[2].value) || '')),
|
||||
pathAsListed: (untagged.attributes[2] && untagged.attributes[2].value) || '',
|
||||
flags: new Set(untagged.attributes[0].map(entry => entry.value)),
|
||||
delimiter: untagged.attributes[1] && untagged.attributes[1].value,
|
||||
listed: true
|
||||
};
|
||||
|
||||
if (specialUseHints[entry.path]) {
|
||||
addSpecialUseMatch(entry, specialUseHints[entry.path], 'user');
|
||||
}
|
||||
|
||||
if (listCommand === 'XLIST' && entry.flags.has('\\Inbox')) {
|
||||
// XLIST specific flag, ignore
|
||||
entry.flags.delete('\\Inbox');
|
||||
if (entry.path !== 'INBOX') {
|
||||
// XLIST may use localised inbox name
|
||||
addSpecialUseMatch(entry, '\\Inbox', 'extension');
|
||||
}
|
||||
}
|
||||
|
||||
if (entry.path.toUpperCase() === 'INBOX') {
|
||||
addSpecialUseMatch(entry, '\\Inbox', 'name');
|
||||
}
|
||||
|
||||
if (entry.delimiter && entry.path.charAt(0) === entry.delimiter) {
|
||||
entry.path = entry.path.slice(1);
|
||||
}
|
||||
|
||||
entry.parentPath = entry.delimiter && entry.path ? entry.path.substr(0, entry.path.lastIndexOf(entry.delimiter)) : '';
|
||||
entry.parent = entry.delimiter ? entry.path.split(entry.delimiter) : [entry.path];
|
||||
entry.name = entry.parent.pop();
|
||||
|
||||
let { flag: specialUseFlag, source: flagSource } = specialUse(
|
||||
connection.capabilities.has('XLIST') || connection.capabilities.has('SPECIAL-USE'),
|
||||
entry
|
||||
);
|
||||
|
||||
if (specialUseFlag) {
|
||||
addSpecialUseMatch(entry, specialUseFlag, flagSource);
|
||||
}
|
||||
|
||||
entries.push(entry);
|
||||
},
|
||||
|
||||
STATUS: async untagged => {
|
||||
let statusPath = normalizePath(connection, decodePath(connection, (untagged.attributes[0] && untagged.attributes[0].value) || ''));
|
||||
let statusList = untagged.attributes && Array.isArray(untagged.attributes[1]) ? untagged.attributes[1] : false;
|
||||
if (!statusList || !statusPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
let key;
|
||||
|
||||
let map = { path: statusPath };
|
||||
|
||||
statusList.forEach((entry, i) => {
|
||||
if (i % 2 === 0) {
|
||||
key = entry && typeof entry.value === 'string' ? entry.value : false;
|
||||
return;
|
||||
}
|
||||
if (!key || !entry || typeof entry.value !== 'string') {
|
||||
return;
|
||||
}
|
||||
let value = false;
|
||||
switch (key.toUpperCase()) {
|
||||
case 'MESSAGES':
|
||||
key = 'messages';
|
||||
value = !isNaN(entry.value) ? Number(entry.value) : false;
|
||||
break;
|
||||
|
||||
case 'RECENT':
|
||||
key = 'recent';
|
||||
value = !isNaN(entry.value) ? Number(entry.value) : false;
|
||||
break;
|
||||
|
||||
case 'UIDNEXT':
|
||||
key = 'uidNext';
|
||||
value = !isNaN(entry.value) ? Number(entry.value) : false;
|
||||
break;
|
||||
|
||||
case 'UIDVALIDITY':
|
||||
key = 'uidValidity';
|
||||
value = !isNaN(entry.value) ? BigInt(entry.value) : false;
|
||||
break;
|
||||
|
||||
case 'UNSEEN':
|
||||
key = 'unseen';
|
||||
value = !isNaN(entry.value) ? Number(entry.value) : false;
|
||||
break;
|
||||
|
||||
case 'HIGHESTMODSEQ':
|
||||
key = 'highestModseq';
|
||||
value = !isNaN(entry.value) ? BigInt(entry.value) : false;
|
||||
break;
|
||||
}
|
||||
if (value === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
map[key] = value;
|
||||
});
|
||||
|
||||
statusMap.set(statusPath, map);
|
||||
}
|
||||
}
|
||||
});
|
||||
response.next();
|
||||
};
|
||||
|
||||
let normalizedReference = normalizePath(connection, reference || '');
|
||||
await runList(normalizedReference, normalizePath(connection, mailbox || '', true));
|
||||
|
||||
if (options.listOnly) {
|
||||
return entries;
|
||||
}
|
||||
|
||||
if (normalizedReference && !specialUseMatches['\\Inbox']) {
|
||||
// INBOX was most probably not included in the listing if namespace was used
|
||||
await runList('', 'INBOX');
|
||||
}
|
||||
|
||||
if (options.statusQuery) {
|
||||
for (let entry of entries) {
|
||||
if (!entry.flags.has('\\Noselect') && !entry.flags.has('\\NonExistent')) {
|
||||
if (statusMap.has(entry.path)) {
|
||||
entry.status = statusMap.get(entry.path);
|
||||
} else if (!statusMap.size) {
|
||||
// run STATUS command
|
||||
try {
|
||||
entry.status = await connection.run('STATUS', entry.path, options.statusQuery);
|
||||
} catch (err) {
|
||||
entry.status = { error: err };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response = await connection.exec(
|
||||
'LSUB',
|
||||
[encodePath(connection, normalizePath(connection, reference || '')), encodePath(connection, normalizePath(connection, mailbox || '', true))],
|
||||
{
|
||||
untagged: {
|
||||
LSUB: async untagged => {
|
||||
if (!untagged.attributes || !untagged.attributes.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let entry = {
|
||||
path: normalizePath(connection, decodePath(connection, (untagged.attributes[2] && untagged.attributes[2].value) || '')),
|
||||
pathAsListed: (untagged.attributes[2] && untagged.attributes[2].value) || '',
|
||||
flags: new Set(untagged.attributes[0].map(entry => entry.value)),
|
||||
delimiter: untagged.attributes[1] && untagged.attributes[1].value,
|
||||
subscribed: true
|
||||
};
|
||||
|
||||
if (entry.path.toUpperCase() === 'INBOX') {
|
||||
addSpecialUseMatch(entry, '\\Inbox', 'name');
|
||||
}
|
||||
|
||||
if (entry.delimiter && entry.path.charAt(0) === entry.delimiter) {
|
||||
entry.path = entry.path.slice(1);
|
||||
}
|
||||
|
||||
entry.parentPath = entry.delimiter && entry.path ? entry.path.substr(0, entry.path.lastIndexOf(entry.delimiter)) : '';
|
||||
entry.parent = entry.delimiter ? entry.path.split(entry.delimiter) : [entry.path];
|
||||
entry.name = entry.parent.pop();
|
||||
|
||||
let existing = entries.find(existing => existing.path === entry.path);
|
||||
if (existing) {
|
||||
existing.subscribed = true;
|
||||
entry.flags.forEach(flag => existing.flags.add(flag));
|
||||
} else {
|
||||
// ignore non-listed folders
|
||||
/*
|
||||
let specialUseFlag = specialUse(connection.capabilities.has('XLIST') || connection.capabilities.has('SPECIAL-USE'), entry);
|
||||
if (specialUseFlag && !flagsSeen.has(specialUseFlag)) {
|
||||
entry.specialUse = specialUseFlag;
|
||||
}
|
||||
entries.push(entry);
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
response.next();
|
||||
|
||||
for (let type of Object.keys(specialUseMatches)) {
|
||||
let sortedEntries = specialUseMatches[type].sort((a, b) => {
|
||||
let aSource = SOURCE_SORT_ORDER.indexOf(a.source);
|
||||
let bSource = SOURCE_SORT_ORDER.indexOf(b.source);
|
||||
if (aSource === bSource) {
|
||||
return a.entry.path.localeCompare(b.entry.path);
|
||||
}
|
||||
return aSource - bSource;
|
||||
});
|
||||
|
||||
if (!sortedEntries[0].entry.specialUse) {
|
||||
sortedEntries[0].entry.specialUse = type;
|
||||
sortedEntries[0].entry.specialUseSource = sortedEntries[0].source;
|
||||
}
|
||||
}
|
||||
|
||||
let inboxEntry = entries.find(entry => entry.specialUse === '\\Inbox');
|
||||
if (inboxEntry && !inboxEntry.subscribed) {
|
||||
// override server settings and make INBOX always as subscribed
|
||||
inboxEntry.subscribed = true;
|
||||
}
|
||||
|
||||
return entries.sort((a, b) => {
|
||||
if (a.specialUse && !b.specialUse) {
|
||||
return -1;
|
||||
}
|
||||
if (!a.specialUse && b.specialUse) {
|
||||
return 1;
|
||||
}
|
||||
if (a.specialUse && b.specialUse) {
|
||||
return FLAG_SORT_ORDER.indexOf(a.specialUse) - FLAG_SORT_ORDER.indexOf(b.specialUse);
|
||||
}
|
||||
|
||||
let aList = [].concat(a.parent).concat(a.name);
|
||||
let bList = [].concat(b.parent).concat(b.name);
|
||||
|
||||
for (let i = 0; i < aList.length; i++) {
|
||||
let aPart = aList[i];
|
||||
let bPart = bList[i];
|
||||
if (aPart !== bPart) {
|
||||
return aPart.localeCompare(bPart || '');
|
||||
}
|
||||
}
|
||||
|
||||
return a.path.localeCompare(b.path);
|
||||
});
|
||||
} catch (err) {
|
||||
connection.log.warn({ msg: 'Failed to list folders', err, cid: connection.id });
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
31
backend/node_modules/imapflow/lib/commands/login.js
generated
vendored
Normal file
31
backend/node_modules/imapflow/lib/commands/login.js
generated
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
'use strict';
|
||||
|
||||
const { getStatusCode, getErrorText } = require('../tools.js');
|
||||
|
||||
// Authenticates user using LOGIN
|
||||
module.exports = async (connection, username, password) => {
|
||||
if (connection.state !== connection.states.NOT_AUTHENTICATED) {
|
||||
// nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let response = await connection.exec('LOGIN', [
|
||||
{ type: 'STRING', value: username },
|
||||
{ type: 'STRING', value: password, sensitive: true }
|
||||
]);
|
||||
response.next();
|
||||
|
||||
connection.authCapabilities.set('LOGIN', true);
|
||||
|
||||
return username;
|
||||
} catch (err) {
|
||||
let errorCode = getStatusCode(err.response);
|
||||
if (errorCode) {
|
||||
err.serverResponseCode = errorCode;
|
||||
}
|
||||
err.authenticationFailed = true;
|
||||
err.response = await getErrorText(err.response);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
34
backend/node_modules/imapflow/lib/commands/logout.js
generated
vendored
Normal file
34
backend/node_modules/imapflow/lib/commands/logout.js
generated
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
'use strict';
|
||||
|
||||
// Logs out user and closes connection
|
||||
module.exports = async connection => {
|
||||
if (connection.state === connection.states.LOGOUT) {
|
||||
// nothing to do here
|
||||
return false;
|
||||
}
|
||||
|
||||
if (connection.state === connection.states.NOT_AUTHENTICATED) {
|
||||
connection.state = connection.states.LOGOUT;
|
||||
connection.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await connection.exec('LOGOUT');
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (err.code === 'NoConnection') {
|
||||
return true;
|
||||
}
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
return false;
|
||||
} finally {
|
||||
// close even if command failed
|
||||
connection.state = connection.states.LOGOUT;
|
||||
if (response && typeof response.next === 'function') {
|
||||
response.next();
|
||||
}
|
||||
connection.close();
|
||||
}
|
||||
};
|
||||
67
backend/node_modules/imapflow/lib/commands/move.js
generated
vendored
Normal file
67
backend/node_modules/imapflow/lib/commands/move.js
generated
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
'use strict';
|
||||
|
||||
const { normalizePath, encodePath, expandRange, enhanceCommandError } = require('../tools.js');
|
||||
|
||||
// Moves messages from current mailbox to some other mailbox
|
||||
module.exports = async (connection, range, destination, options) => {
|
||||
if (connection.state !== connection.states.SELECTED || !range || !destination) {
|
||||
// nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
destination = normalizePath(connection, destination);
|
||||
|
||||
let attributes = [
|
||||
{ type: 'SEQUENCE', value: range },
|
||||
{ type: 'ATOM', value: encodePath(connection, destination) }
|
||||
];
|
||||
|
||||
let map = { path: connection.mailbox.path, destination };
|
||||
|
||||
if (!connection.capabilities.has('MOVE')) {
|
||||
let result = await connection.messageCopy(range, destination, options);
|
||||
await connection.messageDelete(range, Object.assign({ silent: true }, options));
|
||||
return result;
|
||||
}
|
||||
|
||||
let checkMoveInfo = response => {
|
||||
let section = response.attributes && response.attributes[0] && response.attributes[0].section;
|
||||
let responseCode = section && section.length && section[0] && typeof section[0].value === 'string' ? section[0].value : '';
|
||||
switch (responseCode) {
|
||||
case 'COPYUID':
|
||||
{
|
||||
let uidValidity = section[1] && typeof section[1].value === 'string' && !isNaN(section[1].value) ? BigInt(section[1].value) : false;
|
||||
if (uidValidity) {
|
||||
map.uidValidity = uidValidity;
|
||||
}
|
||||
|
||||
let sourceUids = section[2] && typeof section[2].value === 'string' ? expandRange(section[2].value) : false;
|
||||
let destinationUids = section[3] && typeof section[3].value === 'string' ? expandRange(section[3].value) : false;
|
||||
if (sourceUids && destinationUids && sourceUids.length === destinationUids.length) {
|
||||
map.uidMap = new Map(sourceUids.map((uid, i) => [uid, destinationUids[i]]));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await connection.exec(options.uid ? 'UID MOVE' : 'MOVE', attributes, {
|
||||
untagged: {
|
||||
OK: async untagged => {
|
||||
checkMoveInfo(untagged);
|
||||
}
|
||||
}
|
||||
});
|
||||
response.next();
|
||||
|
||||
checkMoveInfo(response.response);
|
||||
return map;
|
||||
} catch (err) {
|
||||
await enhanceCommandError(err);
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
return false;
|
||||
}
|
||||
};
|
||||
107
backend/node_modules/imapflow/lib/commands/namespace.js
generated
vendored
Normal file
107
backend/node_modules/imapflow/lib/commands/namespace.js
generated
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
'use strict';
|
||||
|
||||
// Requests NAMESPACE info from server
|
||||
module.exports = async connection => {
|
||||
if (![connection.states.AUTHENTICATED, connection.states.SELECTED].includes(connection.state)) {
|
||||
// nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
if (!connection.capabilities.has('NAMESPACE')) {
|
||||
// try to derive from listing
|
||||
let { prefix, delimiter } = await getListPrefix(connection);
|
||||
if (delimiter && prefix && prefix.charAt(prefix.length - 1) !== delimiter) {
|
||||
prefix += delimiter;
|
||||
}
|
||||
let map = {
|
||||
personal: [{ prefix: prefix || '', delimiter }],
|
||||
other: false,
|
||||
shared: false
|
||||
};
|
||||
connection.namespaces = map;
|
||||
connection.namespace = connection.namespaces.personal[0];
|
||||
return connection.namespace;
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
let map = {};
|
||||
response = await connection.exec('NAMESPACE', false, {
|
||||
untagged: {
|
||||
NAMESPACE: async untagged => {
|
||||
if (!untagged.attributes || !untagged.attributes.length) {
|
||||
return;
|
||||
}
|
||||
map.personal = getNamsepaceInfo(untagged.attributes[0]);
|
||||
map.other = getNamsepaceInfo(untagged.attributes[1]);
|
||||
map.shared = getNamsepaceInfo(untagged.attributes[2]);
|
||||
}
|
||||
}
|
||||
});
|
||||
connection.namespaces = map;
|
||||
|
||||
// make sure that we have the first personal namespace always set
|
||||
if (!connection.namespaces.personal[0]) {
|
||||
connection.namespaces.personal[0] = { prefix: '', delimiter: '.' };
|
||||
}
|
||||
connection.namespaces.personal[0].prefix = connection.namespaces.personal[0].prefix || '';
|
||||
response.next();
|
||||
|
||||
connection.namespace = connection.namespaces.personal[0];
|
||||
|
||||
return connection.namespace;
|
||||
} catch (err) {
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
return {
|
||||
error: true,
|
||||
status: err.responseStatus,
|
||||
text: err.responseText
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
async function getListPrefix(connection) {
|
||||
let response;
|
||||
try {
|
||||
let map = {};
|
||||
response = await connection.exec('LIST', ['', ''], {
|
||||
untagged: {
|
||||
LIST: async untagged => {
|
||||
if (!untagged.attributes || !untagged.attributes.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
map.flags = new Set(untagged.attributes[0].map(entry => entry.value));
|
||||
map.delimiter = untagged.attributes[1] && untagged.attributes[1].value;
|
||||
map.prefix = (untagged.attributes[2] && untagged.attributes[2].value) || '';
|
||||
if (map.delimiter && map.prefix.charAt(0) === map.delimiter) {
|
||||
map.prefix = map.prefix.slice(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
response.next();
|
||||
return map;
|
||||
} catch (err) {
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function getNamsepaceInfo(attribute) {
|
||||
if (!attribute || !attribute.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return attribute
|
||||
.filter(entry => entry.length >= 2 && typeof entry[0].value === 'string' && typeof entry[1].value === 'string')
|
||||
.map(entry => {
|
||||
let prefix = entry[0].value;
|
||||
let delimiter = entry[1].value;
|
||||
|
||||
if (delimiter && prefix && prefix.charAt(prefix.length - 1) !== delimiter) {
|
||||
prefix += delimiter;
|
||||
}
|
||||
return { prefix, delimiter };
|
||||
});
|
||||
}
|
||||
13
backend/node_modules/imapflow/lib/commands/noop.js
generated
vendored
Normal file
13
backend/node_modules/imapflow/lib/commands/noop.js
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
// Sends a NO-OP command
|
||||
module.exports = async connection => {
|
||||
try {
|
||||
let response = await connection.exec('NOOP', false, { comment: 'Requested by command' });
|
||||
response.next();
|
||||
return true;
|
||||
} catch (err) {
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
return false;
|
||||
}
|
||||
};
|
||||
102
backend/node_modules/imapflow/lib/commands/quota.js
generated
vendored
Normal file
102
backend/node_modules/imapflow/lib/commands/quota.js
generated
vendored
Normal file
@@ -0,0 +1,102 @@
|
||||
'use strict';
|
||||
|
||||
const { encodePath, normalizePath, enhanceCommandError } = require('../tools.js');
|
||||
|
||||
// Requests quota information for a mailbox
|
||||
module.exports = async (connection, path) => {
|
||||
if (![connection.states.AUTHENTICATED, connection.states.SELECTED].includes(connection.state) || !path) {
|
||||
// nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
if (!connection.capabilities.has('QUOTA')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
path = normalizePath(connection, path);
|
||||
|
||||
let map = { path };
|
||||
|
||||
let processQuotaResponse = untagged => {
|
||||
let attributes = untagged.attributes && untagged.attributes[1];
|
||||
if (!attributes || !attributes.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let key = false;
|
||||
attributes.forEach((attribute, i) => {
|
||||
if (i % 3 === 0) {
|
||||
key = attribute && typeof attribute.value === 'string' ? attribute.value.toLowerCase() : false;
|
||||
return;
|
||||
}
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
|
||||
let value = attribute && typeof attribute.value === 'string' && !isNaN(attribute.value) ? Number(attribute.value) : false;
|
||||
if (value === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (i % 3 === 1) {
|
||||
// usage
|
||||
if (!map[key]) {
|
||||
map[key] = {};
|
||||
}
|
||||
map[key].usage = value * (key === 'storage' ? 1024 : 1);
|
||||
}
|
||||
|
||||
if (i % 3 === 2) {
|
||||
// limit
|
||||
if (!map[key]) {
|
||||
map[key] = {};
|
||||
}
|
||||
map[key].limit = value * (key === 'storage' ? 1024 : 1);
|
||||
|
||||
if (map[key].limit) {
|
||||
map[key].status = Math.round(((map[key].usage || 0) / map[key].limit) * 100) + '%';
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
let quotaFound = false;
|
||||
let response;
|
||||
try {
|
||||
response = await connection.exec('GETQUOTAROOT', [{ type: 'ATOM', value: encodePath(connection, path) }], {
|
||||
untagged: {
|
||||
QUOTAROOT: async untagged => {
|
||||
let quotaRoot =
|
||||
untagged.attributes && untagged.attributes[1] && typeof untagged.attributes[1].value === 'string'
|
||||
? untagged.attributes[1].value
|
||||
: false;
|
||||
if (quotaRoot) {
|
||||
map.quotaRoot = quotaRoot;
|
||||
}
|
||||
},
|
||||
QUOTA: async untagged => {
|
||||
quotaFound = true;
|
||||
processQuotaResponse(untagged);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
response.next();
|
||||
|
||||
if (map.quotaRoot && !quotaFound) {
|
||||
response = await connection.exec('GETQUOTA', [{ type: 'ATOM', value: map.quotaRoot }], {
|
||||
untagged: {
|
||||
QUOTA: async untagged => {
|
||||
processQuotaResponse(untagged);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return map;
|
||||
} catch (err) {
|
||||
await enhanceCommandError(err);
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
return false;
|
||||
}
|
||||
};
|
||||
36
backend/node_modules/imapflow/lib/commands/rename.js
generated
vendored
Normal file
36
backend/node_modules/imapflow/lib/commands/rename.js
generated
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
'use strict';
|
||||
|
||||
const { encodePath, normalizePath, enhanceCommandError } = require('../tools.js');
|
||||
|
||||
// Renames existing mailbox
|
||||
module.exports = async (connection, path, newPath) => {
|
||||
if (![connection.states.AUTHENTICATED, connection.states.SELECTED].includes(connection.state)) {
|
||||
// nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
path = normalizePath(connection, path);
|
||||
newPath = normalizePath(connection, newPath);
|
||||
|
||||
if (connection.state === connection.states.SELECTED && connection.mailbox.path === path) {
|
||||
await connection.run('CLOSE');
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
let map = {
|
||||
path,
|
||||
newPath
|
||||
};
|
||||
response = await connection.exec('RENAME', [
|
||||
{ type: 'ATOM', value: encodePath(connection, path) },
|
||||
{ type: 'ATOM', value: encodePath(connection, newPath) }
|
||||
]);
|
||||
response.next();
|
||||
return map;
|
||||
} catch (err) {
|
||||
await enhanceCommandError(err);
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
50
backend/node_modules/imapflow/lib/commands/search.js
generated
vendored
Normal file
50
backend/node_modules/imapflow/lib/commands/search.js
generated
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
'use strict';
|
||||
|
||||
const { enhanceCommandError } = require('../tools.js');
|
||||
const { searchCompiler } = require('../search-compiler.js');
|
||||
|
||||
// Updates flags for a message
|
||||
module.exports = async (connection, query, options) => {
|
||||
if (connection.state !== connection.states.SELECTED) {
|
||||
// nothing to do here
|
||||
return false;
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
|
||||
let attributes;
|
||||
|
||||
if (!query || query === true || (typeof query === 'object' && (!Object.keys(query).length || (Object.keys(query).length === 1 && query.all)))) {
|
||||
// search for all messages
|
||||
attributes = [{ type: 'ATOM', value: 'ALL' }];
|
||||
} else if (query && typeof query === 'object') {
|
||||
// normal query
|
||||
attributes = searchCompiler(connection, query);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
let results = new Set();
|
||||
let response;
|
||||
try {
|
||||
response = await connection.exec(options.uid ? 'UID SEARCH' : 'SEARCH', attributes, {
|
||||
untagged: {
|
||||
SEARCH: async untagged => {
|
||||
if (untagged && untagged.attributes && untagged.attributes.length) {
|
||||
untagged.attributes.forEach(attribute => {
|
||||
if (attribute && attribute.value && typeof attribute.value === 'string' && !isNaN(attribute.value)) {
|
||||
results.add(Number(attribute.value));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
response.next();
|
||||
return Array.from(results).sort((a, b) => a - b);
|
||||
} catch (err) {
|
||||
await enhanceCommandError(err);
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
return false;
|
||||
}
|
||||
};
|
||||
216
backend/node_modules/imapflow/lib/commands/select.js
generated
vendored
Normal file
216
backend/node_modules/imapflow/lib/commands/select.js
generated
vendored
Normal file
@@ -0,0 +1,216 @@
|
||||
'use strict';
|
||||
|
||||
const { encodePath, normalizePath, enhanceCommandError } = require('../tools.js');
|
||||
|
||||
// Selects a mailbox
|
||||
module.exports = async (connection, path, options) => {
|
||||
if (![connection.states.AUTHENTICATED, connection.states.SELECTED].includes(connection.state)) {
|
||||
// nothing to do here
|
||||
return;
|
||||
}
|
||||
options = options || {};
|
||||
|
||||
path = normalizePath(connection, path);
|
||||
|
||||
if (!connection.folders.has(path)) {
|
||||
let folders = await connection.run('LIST', '', path);
|
||||
if (!folders) {
|
||||
throw new Error('Failed to fetch folders');
|
||||
}
|
||||
folders.forEach(folder => {
|
||||
connection.folders.set(folder.path, folder);
|
||||
});
|
||||
}
|
||||
|
||||
let folderListData = connection.folders.has(path) ? connection.folders.get(path) : false;
|
||||
|
||||
let response;
|
||||
try {
|
||||
let map = { path };
|
||||
if (folderListData) {
|
||||
['delimiter', 'specialUse', 'subscribed', 'listed'].forEach(key => {
|
||||
if (folderListData[key]) {
|
||||
map[key] = folderListData[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let extraArgs = [];
|
||||
if (connection.enabled.has('QRESYNC') && options.changedSince && options.uidValidity) {
|
||||
extraArgs.push([
|
||||
{ type: 'ATOM', value: 'QRESYNC' },
|
||||
[
|
||||
{ type: 'ATOM', value: options.uidValidity?.toString() },
|
||||
{ type: 'ATOM', value: options.changedSince.toString() }
|
||||
]
|
||||
]);
|
||||
map.qresync = true;
|
||||
}
|
||||
|
||||
let encodedPath = encodePath(connection, path);
|
||||
|
||||
let selectCommand = {
|
||||
command: !options.readOnly ? 'SELECT' : 'EXAMINE',
|
||||
arguments: [{ type: encodedPath.indexOf('&') >= 0 ? 'STRING' : 'ATOM', value: encodedPath }].concat(extraArgs || [])
|
||||
};
|
||||
|
||||
response = await connection.exec(selectCommand.command, selectCommand.arguments, {
|
||||
untagged: {
|
||||
OK: async untagged => {
|
||||
if (!untagged.attributes || !untagged.attributes.length) {
|
||||
return;
|
||||
}
|
||||
let section = !untagged.attributes[0].value && untagged.attributes[0].section;
|
||||
if (section && section.length > 1 && section[0].type === 'ATOM' && typeof section[0].value === 'string') {
|
||||
let key = section[0].value.toLowerCase();
|
||||
let value;
|
||||
|
||||
if (typeof section[1].value === 'string') {
|
||||
value = section[1].value;
|
||||
} else if (Array.isArray(section[1])) {
|
||||
value = section[1].map(entry => (typeof entry.value === 'string' ? entry.value : false)).filter(entry => entry);
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case 'highestmodseq':
|
||||
key = 'highestModseq';
|
||||
if (/^[0-9]+$/.test(value)) {
|
||||
value = BigInt(value);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'mailboxid':
|
||||
key = 'mailboxId';
|
||||
if (Array.isArray(value) && value.length) {
|
||||
value = value[0];
|
||||
}
|
||||
break;
|
||||
|
||||
case 'permanentflags':
|
||||
key = 'permanentFlags';
|
||||
value = new Set(value);
|
||||
break;
|
||||
|
||||
case 'uidnext':
|
||||
key = 'uidNext';
|
||||
value = Number(value);
|
||||
break;
|
||||
|
||||
case 'uidvalidity':
|
||||
key = 'uidValidity';
|
||||
if (/^[0-9]+$/.test(value)) {
|
||||
value = BigInt(value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
map[key] = value;
|
||||
}
|
||||
|
||||
if (section && section.length === 1 && section[0].type === 'ATOM' && typeof section[0].value === 'string') {
|
||||
let key = section[0].value.toLowerCase();
|
||||
switch (key) {
|
||||
case 'nomodseq':
|
||||
key = 'noModseq';
|
||||
map[key] = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
FLAGS: async untagged => {
|
||||
if (!untagged.attributes || (!untagged.attributes.length && Array.isArray(untagged.attributes[0]))) {
|
||||
return;
|
||||
}
|
||||
let flags = untagged.attributes[0].map(flag => (typeof flag.value === 'string' ? flag.value : false)).filter(flag => flag);
|
||||
map.flags = new Set(flags);
|
||||
},
|
||||
EXISTS: async untagged => {
|
||||
let num = Number(untagged.command);
|
||||
if (isNaN(num)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
map.exists = num;
|
||||
},
|
||||
VANISHED: async untagged => {
|
||||
await connection.untaggedVanished(
|
||||
untagged,
|
||||
// mailbox is not yet open, so use a dummy mailbox object
|
||||
{ path, uidNext: false, uidValidity: false }
|
||||
);
|
||||
},
|
||||
// we should only get an untagged FETCH for a SELECT/EXAMINE if QRESYNC was asked for
|
||||
FETCH: async untagged => {
|
||||
await connection.untaggedFetch(
|
||||
untagged,
|
||||
// mailbox is not yet open, so use a dummy mailbox object
|
||||
{ path, uidNext: false, uidValidity: false }
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let section = !response.response.attributes[0].value && response.response.attributes[0].section;
|
||||
if (section && section.length && section[0].type === 'ATOM' && typeof section[0].value === 'string') {
|
||||
switch (section[0].value.toUpperCase()) {
|
||||
case 'READ-ONLY':
|
||||
map.readOnly = true;
|
||||
break;
|
||||
case 'READ-WRITE':
|
||||
default:
|
||||
map.readOnly = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
map.qresync &&
|
||||
// UIDVALIDITY must be the same
|
||||
(options.uidValidity !== map.uidValidity ||
|
||||
// HIGHESTMODSEQ response must be present
|
||||
!map.highestModseq ||
|
||||
// NOMODSEQ is not allowed
|
||||
map.noModseq)
|
||||
) {
|
||||
// QRESYNC does not apply here, so unset it
|
||||
map.qresync = false;
|
||||
}
|
||||
|
||||
let currentMailbox = connection.mailbox;
|
||||
connection.mailbox = false;
|
||||
|
||||
if (currentMailbox && currentMailbox.path !== path) {
|
||||
connection.emit('mailboxClose', currentMailbox);
|
||||
}
|
||||
|
||||
connection.mailbox = map;
|
||||
connection.currentSelectCommand = selectCommand;
|
||||
connection.state = connection.states.SELECTED;
|
||||
|
||||
if (!currentMailbox || currentMailbox.path !== path) {
|
||||
connection.emit('mailboxOpen', connection.mailbox);
|
||||
}
|
||||
|
||||
response.next();
|
||||
return map;
|
||||
} catch (err) {
|
||||
await enhanceCommandError(err);
|
||||
|
||||
if (connection.state === connection.states.SELECTED) {
|
||||
// reset selected state
|
||||
|
||||
let currentMailbox = connection.mailbox;
|
||||
|
||||
connection.mailbox = false;
|
||||
connection.currentSelectCommand = false;
|
||||
connection.state = connection.states.AUTHENTICATED;
|
||||
|
||||
if (currentMailbox) {
|
||||
connection.emit('mailboxClose', currentMailbox);
|
||||
}
|
||||
}
|
||||
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
19
backend/node_modules/imapflow/lib/commands/starttls.js
generated
vendored
Normal file
19
backend/node_modules/imapflow/lib/commands/starttls.js
generated
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
'use strict';
|
||||
|
||||
// Requests STARTTLS info from server
|
||||
module.exports = async connection => {
|
||||
if (!connection.capabilities.has('STARTTLS') || connection.secureConnection) {
|
||||
// nothing to do here
|
||||
return false;
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await connection.exec('STARTTLS');
|
||||
response.next();
|
||||
return true;
|
||||
} catch (err) {
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
return false;
|
||||
}
|
||||
};
|
||||
143
backend/node_modules/imapflow/lib/commands/status.js
generated
vendored
Normal file
143
backend/node_modules/imapflow/lib/commands/status.js
generated
vendored
Normal file
@@ -0,0 +1,143 @@
|
||||
'use strict';
|
||||
|
||||
const { encodePath, normalizePath } = require('../tools.js');
|
||||
|
||||
// Requests info about a mailbox
|
||||
module.exports = async (connection, path, query) => {
|
||||
if (![connection.states.AUTHENTICATED, connection.states.SELECTED].includes(connection.state) || !path) {
|
||||
// nothing to do here
|
||||
return false;
|
||||
}
|
||||
|
||||
path = normalizePath(connection, path);
|
||||
let encodedPath = encodePath(connection, path);
|
||||
|
||||
let attributes = [{ type: encodedPath.indexOf('&') >= 0 ? 'STRING' : 'ATOM', value: encodedPath }];
|
||||
|
||||
let queryAttributes = [];
|
||||
Object.keys(query || {}).forEach(key => {
|
||||
if (!query[key]) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (key.toUpperCase()) {
|
||||
case 'MESSAGES':
|
||||
case 'RECENT':
|
||||
case 'UIDNEXT':
|
||||
case 'UIDVALIDITY':
|
||||
case 'UNSEEN':
|
||||
queryAttributes.push({ type: 'ATOM', value: key.toUpperCase() });
|
||||
break;
|
||||
|
||||
case 'HIGHESTMODSEQ':
|
||||
if (connection.capabilities.has('CONDSTORE')) {
|
||||
queryAttributes.push({ type: 'ATOM', value: key.toUpperCase() });
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
if (!queryAttributes.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
attributes.push(queryAttributes);
|
||||
|
||||
let response;
|
||||
try {
|
||||
let map = { path };
|
||||
response = await connection.exec('STATUS', attributes, {
|
||||
untagged: {
|
||||
STATUS: async untagged => {
|
||||
// If STATUS is for current mailbox then update mailbox values
|
||||
let updateCurrent = connection.state === connection.states.SELECTED && path === connection.mailbox.path;
|
||||
|
||||
let list = untagged.attributes && Array.isArray(untagged.attributes[1]) ? untagged.attributes[1] : false;
|
||||
if (!list) {
|
||||
return;
|
||||
}
|
||||
let key;
|
||||
list.forEach((entry, i) => {
|
||||
if (i % 2 === 0) {
|
||||
key = entry && typeof entry.value === 'string' ? entry.value : false;
|
||||
return;
|
||||
}
|
||||
if (!key || !entry || typeof entry.value !== 'string') {
|
||||
return;
|
||||
}
|
||||
let value = false;
|
||||
switch (key.toUpperCase()) {
|
||||
case 'MESSAGES':
|
||||
key = 'messages';
|
||||
value = !isNaN(entry.value) ? Number(entry.value) : false;
|
||||
if (updateCurrent) {
|
||||
let prevCount = connection.mailbox.exists;
|
||||
if (prevCount !== value) {
|
||||
// somehow message count in current folder has changed?
|
||||
connection.mailbox.exists = value;
|
||||
connection.emit('exists', {
|
||||
path,
|
||||
count: value,
|
||||
prevCount
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'RECENT':
|
||||
key = 'recent';
|
||||
value = !isNaN(entry.value) ? Number(entry.value) : false;
|
||||
break;
|
||||
|
||||
case 'UIDNEXT':
|
||||
key = 'uidNext';
|
||||
value = !isNaN(entry.value) ? Number(entry.value) : false;
|
||||
if (updateCurrent) {
|
||||
connection.mailbox.uidNext = value;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'UIDVALIDITY':
|
||||
key = 'uidValidity';
|
||||
value = !isNaN(entry.value) ? BigInt(entry.value) : false;
|
||||
break;
|
||||
|
||||
case 'UNSEEN':
|
||||
key = 'unseen';
|
||||
value = !isNaN(entry.value) ? Number(entry.value) : false;
|
||||
break;
|
||||
|
||||
case 'HIGHESTMODSEQ':
|
||||
key = 'highestModseq';
|
||||
value = !isNaN(entry.value) ? BigInt(entry.value) : false;
|
||||
if (updateCurrent) {
|
||||
connection.mailbox.highestModseq = value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (value === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
map[key] = value;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
response.next();
|
||||
return map;
|
||||
} catch (err) {
|
||||
if (err.responseStatus === 'NO') {
|
||||
let folders = await connection.run('LIST', '', path, { listOnly: true });
|
||||
if (folders && !folders.length) {
|
||||
let error = new Error(`Mailbox doesn't exist: ${path}`);
|
||||
error.code = 'NotFound';
|
||||
error.response = err;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
return false;
|
||||
}
|
||||
};
|
||||
79
backend/node_modules/imapflow/lib/commands/store.js
generated
vendored
Normal file
79
backend/node_modules/imapflow/lib/commands/store.js
generated
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
'use strict';
|
||||
|
||||
const { formatFlag, canUseFlag, enhanceCommandError } = require('../tools.js');
|
||||
|
||||
// Updates flags for a message
|
||||
module.exports = async (connection, range, flags, options) => {
|
||||
if (connection.state !== connection.states.SELECTED || !range || (options.useLabels && !connection.capabilities.has('X-GM-EXT-1'))) {
|
||||
// nothing to do here
|
||||
return false;
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
let operation;
|
||||
|
||||
operation = 'FLAGS';
|
||||
|
||||
if (options.useLabels) {
|
||||
operation = 'X-GM-LABELS';
|
||||
} else if (options.silent) {
|
||||
operation = `${operation}.SILENT`;
|
||||
}
|
||||
|
||||
switch ((options.operation || '').toLowerCase()) {
|
||||
case 'set':
|
||||
// do nothing, keep operation value as is
|
||||
break;
|
||||
case 'remove':
|
||||
operation = `-${operation}`;
|
||||
break;
|
||||
case 'add':
|
||||
default:
|
||||
operation = `+${operation}`;
|
||||
break;
|
||||
}
|
||||
|
||||
flags = (Array.isArray(flags) ? flags : [].concat(flags || []))
|
||||
.map(flag => {
|
||||
flag = formatFlag(flag);
|
||||
|
||||
if (!canUseFlag(connection.mailbox, flag) && operation !== 'remove') {
|
||||
// it does not seem that we can set this flag
|
||||
return false;
|
||||
}
|
||||
|
||||
return flag;
|
||||
})
|
||||
.filter(flag => flag);
|
||||
|
||||
if (!flags.length && options.operation !== 'set') {
|
||||
// nothing to do here
|
||||
return false;
|
||||
}
|
||||
|
||||
let attributes = [{ type: 'SEQUENCE', value: range }, { type: 'ATOM', value: operation }, flags.map(flag => ({ type: 'ATOM', value: flag }))];
|
||||
|
||||
if (options.unchangedSince && connection.enabled.has('CONDSTORE') && !connection.mailbox.noModseq) {
|
||||
attributes.push([
|
||||
{
|
||||
type: 'ATOM',
|
||||
value: 'UNCHANGEDSINCE'
|
||||
},
|
||||
{
|
||||
type: 'ATOM',
|
||||
value: options.unchangedSince.toString()
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await connection.exec(options.uid ? 'UID STORE' : 'STORE', attributes);
|
||||
response.next();
|
||||
return true;
|
||||
} catch (err) {
|
||||
await enhanceCommandError(err);
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
return false;
|
||||
}
|
||||
};
|
||||
24
backend/node_modules/imapflow/lib/commands/subscribe.js
generated
vendored
Normal file
24
backend/node_modules/imapflow/lib/commands/subscribe.js
generated
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
'use strict';
|
||||
|
||||
const { encodePath, normalizePath, enhanceCommandError } = require('../tools.js');
|
||||
|
||||
// Subscribes to a mailbox
|
||||
module.exports = async (connection, path) => {
|
||||
if (![connection.states.AUTHENTICATED, connection.states.SELECTED].includes(connection.state)) {
|
||||
// nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
path = normalizePath(connection, path);
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await connection.exec('SUBSCRIBE', [{ type: 'ATOM', value: encodePath(connection, path) }]);
|
||||
response.next();
|
||||
return true;
|
||||
} catch (err) {
|
||||
await enhanceCommandError(err);
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
return false;
|
||||
}
|
||||
};
|
||||
24
backend/node_modules/imapflow/lib/commands/unsubscribe.js
generated
vendored
Normal file
24
backend/node_modules/imapflow/lib/commands/unsubscribe.js
generated
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
'use strict';
|
||||
|
||||
const { encodePath, normalizePath, enhanceCommandError } = require('../tools.js');
|
||||
|
||||
// Unsubscribes from a mailbox
|
||||
module.exports = async (connection, path) => {
|
||||
if (![connection.states.AUTHENTICATED, connection.states.SELECTED].includes(connection.state)) {
|
||||
// nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
path = normalizePath(connection, path);
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await connection.exec('UNSUBSCRIBE', [{ type: 'ATOM', value: encodePath(connection, path) }]);
|
||||
response.next();
|
||||
return true;
|
||||
} catch (err) {
|
||||
await enhanceCommandError(err);
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
return false;
|
||||
}
|
||||
};
|
||||
190
backend/node_modules/imapflow/lib/handler/imap-compiler.js
generated
vendored
Normal file
190
backend/node_modules/imapflow/lib/handler/imap-compiler.js
generated
vendored
Normal file
@@ -0,0 +1,190 @@
|
||||
/* eslint no-console: 0, new-cap: 0 */
|
||||
|
||||
'use strict';
|
||||
|
||||
const imapFormalSyntax = require('./imap-formal-syntax');
|
||||
|
||||
const formatRespEntry = (entry, returnEmpty) => {
|
||||
if (typeof entry === 'string') {
|
||||
return Buffer.from(entry);
|
||||
}
|
||||
|
||||
if (typeof entry === 'number') {
|
||||
return Buffer.from(entry.toString());
|
||||
}
|
||||
|
||||
if (Buffer.isBuffer(entry)) {
|
||||
return entry;
|
||||
}
|
||||
|
||||
if (returnEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Buffer.alloc(0);
|
||||
};
|
||||
|
||||
/**
|
||||
* Compiles an input object into
|
||||
*/
|
||||
module.exports = async (response, options) => {
|
||||
let { asArray, isLogging, literalPlus, literalMinus } = options || {};
|
||||
const respParts = [];
|
||||
|
||||
let resp = [].concat(formatRespEntry(response.tag, true) || []).concat(response.command ? formatRespEntry(' ' + response.command) : []);
|
||||
let val;
|
||||
let lastType;
|
||||
|
||||
let walk = async (node, options) => {
|
||||
options = options || {};
|
||||
|
||||
let lastRespEntry = resp.length && resp[resp.length - 1];
|
||||
let lastRespByte = (lastRespEntry && lastRespEntry.length && lastRespEntry[lastRespEntry.length - 1]) || '';
|
||||
if (typeof lastRespByte === 'number') {
|
||||
lastRespByte = String.fromCharCode(lastRespByte);
|
||||
}
|
||||
|
||||
if (lastType === 'LITERAL' || (!['(', '<', '['].includes(lastRespByte) && resp.length)) {
|
||||
if (options.subArray) {
|
||||
// ignore separator
|
||||
} else {
|
||||
resp.push(formatRespEntry(' '));
|
||||
}
|
||||
}
|
||||
|
||||
if (node && node.buffer && !Buffer.isBuffer(node)) {
|
||||
// mongodb binary
|
||||
node = node.buffer;
|
||||
}
|
||||
|
||||
if (Array.isArray(node)) {
|
||||
lastType = 'LIST';
|
||||
resp.push(formatRespEntry('('));
|
||||
|
||||
// check if we need to skip separator WS between two arrays
|
||||
let subArray = node.length > 1 && Array.isArray(node[0]);
|
||||
|
||||
for (let child of node) {
|
||||
if (subArray && !Array.isArray(child)) {
|
||||
subArray = false;
|
||||
}
|
||||
await walk(child, { subArray });
|
||||
}
|
||||
|
||||
resp.push(formatRespEntry(')'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!node && typeof node !== 'string' && typeof node !== 'number' && !Buffer.isBuffer(node)) {
|
||||
resp.push(formatRespEntry('NIL'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof node === 'string' || Buffer.isBuffer(node)) {
|
||||
if (isLogging && node.length > 100) {
|
||||
resp.push(formatRespEntry('"(* ' + node.length + 'B string *)"'));
|
||||
} else {
|
||||
resp.push(formatRespEntry(JSON.stringify(node.toString())));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof node === 'number') {
|
||||
resp.push(formatRespEntry(Math.round(node) || 0)); // Only integers allowed
|
||||
return;
|
||||
}
|
||||
|
||||
lastType = node.type;
|
||||
|
||||
if (isLogging && node.sensitive) {
|
||||
resp.push(formatRespEntry('"(* value hidden *)"'));
|
||||
return;
|
||||
}
|
||||
|
||||
switch (node.type.toUpperCase()) {
|
||||
case 'LITERAL':
|
||||
if (isLogging) {
|
||||
resp.push(formatRespEntry('"(* ' + node.value.length + 'B literal *)"'));
|
||||
} else {
|
||||
let literalLength = !node.value ? 0 : Math.max(node.value.length, 0);
|
||||
|
||||
let canAppend = !asArray || literalPlus || (literalMinus && literalLength <= 4096);
|
||||
let usePlus = canAppend && (literalMinus || literalPlus);
|
||||
|
||||
resp.push(formatRespEntry(`${node.isLiteral8 ? '~' : ''}{${literalLength}${usePlus ? '+' : ''}}\r\n`));
|
||||
|
||||
if (canAppend) {
|
||||
if (node.value && node.value.length) {
|
||||
resp.push(formatRespEntry(node.value));
|
||||
}
|
||||
} else {
|
||||
respParts.push(resp);
|
||||
resp = [].concat(formatRespEntry(node.value, true) || []);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'STRING':
|
||||
if (isLogging && node.value.length > 100) {
|
||||
resp.push(formatRespEntry('"(* ' + node.value.length + 'B string *)"'));
|
||||
} else {
|
||||
resp.push(formatRespEntry(JSON.stringify((node.value || '').toString())));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'TEXT':
|
||||
case 'SEQUENCE':
|
||||
if (node.value) {
|
||||
resp.push(formatRespEntry(node.value));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'NUMBER':
|
||||
resp.push(formatRespEntry(node.value || 0));
|
||||
break;
|
||||
|
||||
case 'ATOM':
|
||||
case 'SECTION':
|
||||
val = (node.value || '').toString();
|
||||
|
||||
if (!node.section || val) {
|
||||
if (node.value === '' || imapFormalSyntax.verify(val.charAt(0) === '\\' ? val.substr(1) : val, imapFormalSyntax['ATOM-CHAR']()) >= 0) {
|
||||
val = JSON.stringify(val);
|
||||
}
|
||||
|
||||
resp.push(formatRespEntry(val));
|
||||
}
|
||||
|
||||
if (node.section) {
|
||||
resp.push(formatRespEntry('['));
|
||||
|
||||
for (let child of node.section) {
|
||||
await walk(child);
|
||||
}
|
||||
|
||||
resp.push(formatRespEntry(']'));
|
||||
}
|
||||
if (node.partial) {
|
||||
resp.push(formatRespEntry(`<${node.partial.join('.')}>`));
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
if (response.attributes) {
|
||||
let attributes = Array.isArray(response.attributes) ? response.attributes : [].concat(response.attributes);
|
||||
for (let child of attributes) {
|
||||
await walk(child);
|
||||
}
|
||||
}
|
||||
|
||||
if (resp.length) {
|
||||
respParts.push(resp);
|
||||
}
|
||||
|
||||
for (let i = 0; i < respParts.length; i++) {
|
||||
respParts[i] = Buffer.concat(respParts[i]);
|
||||
}
|
||||
|
||||
return asArray ? respParts : respParts.flatMap(entry => entry);
|
||||
};
|
||||
147
backend/node_modules/imapflow/lib/handler/imap-formal-syntax.js
generated
vendored
Normal file
147
backend/node_modules/imapflow/lib/handler/imap-formal-syntax.js
generated
vendored
Normal file
@@ -0,0 +1,147 @@
|
||||
/* eslint object-shorthand:0, new-cap: 0, no-useless-concat: 0 */
|
||||
|
||||
'use strict';
|
||||
|
||||
// IMAP Formal Syntax
|
||||
// http://tools.ietf.org/html/rfc3501#section-9
|
||||
|
||||
function expandRange(start, end) {
|
||||
let chars = [];
|
||||
for (let i = start; i <= end; i++) {
|
||||
chars.push(i);
|
||||
}
|
||||
return String.fromCharCode(...chars);
|
||||
}
|
||||
|
||||
function excludeChars(source, exclude) {
|
||||
let sourceArr = Array.prototype.slice.call(source);
|
||||
for (let i = sourceArr.length - 1; i >= 0; i--) {
|
||||
if (exclude.indexOf(sourceArr[i]) >= 0) {
|
||||
sourceArr.splice(i, 1);
|
||||
}
|
||||
}
|
||||
return sourceArr.join('');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
CHAR() {
|
||||
let value = expandRange(0x01, 0x7f);
|
||||
this.CHAR = function () {
|
||||
return value;
|
||||
};
|
||||
return value;
|
||||
},
|
||||
|
||||
CHAR8() {
|
||||
let value = expandRange(0x01, 0xff);
|
||||
this.CHAR8 = function () {
|
||||
return value;
|
||||
};
|
||||
return value;
|
||||
},
|
||||
|
||||
SP() {
|
||||
return ' ';
|
||||
},
|
||||
|
||||
CTL() {
|
||||
let value = expandRange(0x00, 0x1f) + '\x7F';
|
||||
this.CTL = function () {
|
||||
return value;
|
||||
};
|
||||
return value;
|
||||
},
|
||||
|
||||
DQUOTE() {
|
||||
return '"';
|
||||
},
|
||||
|
||||
ALPHA() {
|
||||
let value = expandRange(0x41, 0x5a) + expandRange(0x61, 0x7a);
|
||||
this.ALPHA = function () {
|
||||
return value;
|
||||
};
|
||||
return value;
|
||||
},
|
||||
|
||||
DIGIT() {
|
||||
let value = expandRange(0x30, 0x39);
|
||||
this.DIGIT = function () {
|
||||
return value;
|
||||
};
|
||||
return value;
|
||||
},
|
||||
|
||||
'ATOM-CHAR'() {
|
||||
let value = excludeChars(this.CHAR(), this['atom-specials']());
|
||||
this['ATOM-CHAR'] = function () {
|
||||
return value;
|
||||
};
|
||||
return value;
|
||||
},
|
||||
|
||||
'ASTRING-CHAR'() {
|
||||
let value = this['ATOM-CHAR']() + this['resp-specials']();
|
||||
this['ASTRING-CHAR'] = function () {
|
||||
return value;
|
||||
};
|
||||
return value;
|
||||
},
|
||||
|
||||
'TEXT-CHAR'() {
|
||||
let value = excludeChars(this.CHAR(), '\r\n');
|
||||
this['TEXT-CHAR'] = function () {
|
||||
return value;
|
||||
};
|
||||
return value;
|
||||
},
|
||||
|
||||
'atom-specials'() {
|
||||
let value = '(' + ')' + '{' + this.SP() + this.CTL() + this['list-wildcards']() + this['quoted-specials']() + this['resp-specials']();
|
||||
this['atom-specials'] = function () {
|
||||
return value;
|
||||
};
|
||||
return value;
|
||||
},
|
||||
|
||||
'list-wildcards'() {
|
||||
return '%' + '*';
|
||||
},
|
||||
|
||||
'quoted-specials'() {
|
||||
let value = this.DQUOTE() + '\\';
|
||||
this['quoted-specials'] = function () {
|
||||
return value;
|
||||
};
|
||||
return value;
|
||||
},
|
||||
|
||||
'resp-specials'() {
|
||||
return ']';
|
||||
},
|
||||
|
||||
tag() {
|
||||
let value = excludeChars(this['ASTRING-CHAR'](), '+');
|
||||
this.tag = function () {
|
||||
return value;
|
||||
};
|
||||
return value;
|
||||
},
|
||||
|
||||
command() {
|
||||
let value = this.ALPHA() + this.DIGIT() + '-';
|
||||
this.command = function () {
|
||||
return value;
|
||||
};
|
||||
return value;
|
||||
},
|
||||
|
||||
verify(str, allowedChars) {
|
||||
for (let i = 0, len = str.length; i < len; i++) {
|
||||
if (allowedChars.indexOf(str.charAt(i)) < 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
9
backend/node_modules/imapflow/lib/handler/imap-handler.js
generated
vendored
Normal file
9
backend/node_modules/imapflow/lib/handler/imap-handler.js
generated
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
const parser = require('./imap-parser');
|
||||
const compiler = require('./imap-compiler');
|
||||
|
||||
module.exports = {
|
||||
parser,
|
||||
compiler
|
||||
};
|
||||
67
backend/node_modules/imapflow/lib/handler/imap-parser.js
generated
vendored
Normal file
67
backend/node_modules/imapflow/lib/handler/imap-parser.js
generated
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
'use strict';
|
||||
|
||||
const imapFormalSyntax = require('./imap-formal-syntax');
|
||||
const { ParserInstance } = require('./parser-instance');
|
||||
|
||||
module.exports = async (command, options) => {
|
||||
options = options || {};
|
||||
|
||||
let nullBytesRemoved = 0;
|
||||
|
||||
// special case with a buggy IMAP server where responses are padded with zero bytes
|
||||
if (command[0] === 0) {
|
||||
// find the first non null byte and trim
|
||||
let firstNonNull = -1;
|
||||
for (let i = 0; i < command.length; i++) {
|
||||
if (command[i] !== 0) {
|
||||
firstNonNull = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (firstNonNull === -1) {
|
||||
// All bytes are null
|
||||
return { tag: '*', command: 'BAD', attributes: [] };
|
||||
}
|
||||
command = command.slice(firstNonNull);
|
||||
nullBytesRemoved = firstNonNull;
|
||||
}
|
||||
|
||||
const parser = new ParserInstance(command, options);
|
||||
const response = {};
|
||||
|
||||
try {
|
||||
response.tag = await parser.getTag();
|
||||
|
||||
await parser.getSpace();
|
||||
|
||||
response.command = await parser.getCommand();
|
||||
|
||||
if (nullBytesRemoved) {
|
||||
response.nullBytesRemoved = nullBytesRemoved;
|
||||
}
|
||||
|
||||
if (['UID', 'AUTHENTICATE'].indexOf((response.command || '').toUpperCase()) >= 0) {
|
||||
await parser.getSpace();
|
||||
response.command += ' ' + (await parser.getElement(imapFormalSyntax.command()));
|
||||
}
|
||||
|
||||
if (parser.remainder.trim().length) {
|
||||
await parser.getSpace();
|
||||
response.attributes = await parser.getAttributes();
|
||||
}
|
||||
|
||||
if (parser.humanReadable) {
|
||||
response.attributes = (response.attributes || []).concat({
|
||||
type: 'TEXT',
|
||||
value: parser.humanReadable
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.code === 'ParserErrorExchange' && err.parserContext && err.parserContext.value) {
|
||||
return err.parserContext.value;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
257
backend/node_modules/imapflow/lib/handler/imap-stream.js
generated
vendored
Normal file
257
backend/node_modules/imapflow/lib/handler/imap-stream.js
generated
vendored
Normal file
@@ -0,0 +1,257 @@
|
||||
'use strict';
|
||||
|
||||
const Transform = require('stream').Transform;
|
||||
const logger = require('../logger');
|
||||
|
||||
const LINE = 0x01;
|
||||
const LITERAL = 0x02;
|
||||
|
||||
const LF = 0x0a;
|
||||
const CR = 0x0d;
|
||||
const NUM_0 = 0x30;
|
||||
const NUM_9 = 0x39;
|
||||
const CURLY_OPEN = 0x7b;
|
||||
const CURLY_CLOSE = 0x7d;
|
||||
|
||||
// Maximum allowed literal size: 1GB (1073741824 bytes)
|
||||
const MAX_LITERAL_SIZE = 1024 * 1024 * 1024;
|
||||
|
||||
class ImapStream extends Transform {
|
||||
constructor(options) {
|
||||
super({
|
||||
//writableHighWaterMark: 3,
|
||||
readableObjectMode: true,
|
||||
writableObjectMode: false
|
||||
});
|
||||
|
||||
this.options = options || {};
|
||||
this.cid = this.options.cid;
|
||||
|
||||
this.log =
|
||||
this.options.logger && typeof this.options.logger === 'object'
|
||||
? this.options.logger
|
||||
: logger.child({
|
||||
component: 'imap-connection',
|
||||
cid: this.cid
|
||||
});
|
||||
|
||||
this.readBytesCounter = 0;
|
||||
|
||||
this.state = LINE;
|
||||
this.literalWaiting = 0;
|
||||
this.inputBuffer = []; // lines
|
||||
this.lineBuffer = []; // current line
|
||||
this.literalBuffer = [];
|
||||
this.literals = [];
|
||||
|
||||
this.compress = false;
|
||||
this.secureConnection = this.options.secureConnection;
|
||||
|
||||
this.processingInput = false;
|
||||
this.inputQueue = []; // unprocessed input chunks
|
||||
}
|
||||
|
||||
checkLiteralMarker(line) {
|
||||
if (!line || !line.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let pos = line.length - 1;
|
||||
|
||||
if (line[pos] === LF) {
|
||||
pos--;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
if (pos >= 0 && line[pos] === CR) {
|
||||
pos--;
|
||||
}
|
||||
if (pos < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!pos || line[pos] !== CURLY_CLOSE) {
|
||||
return false;
|
||||
}
|
||||
pos--;
|
||||
|
||||
let numBytes = [];
|
||||
for (; pos > 0; pos--) {
|
||||
let c = line[pos];
|
||||
if (c >= NUM_0 && c <= NUM_9) {
|
||||
numBytes.unshift(c);
|
||||
continue;
|
||||
}
|
||||
if (c === CURLY_OPEN && numBytes.length) {
|
||||
const literalSize = Number(Buffer.from(numBytes).toString());
|
||||
|
||||
if (literalSize > MAX_LITERAL_SIZE) {
|
||||
const err = new Error(`Literal size ${literalSize} exceeds maximum allowed size of ${MAX_LITERAL_SIZE} bytes`);
|
||||
err.code = 'LiteralTooLarge';
|
||||
err.literalSize = literalSize;
|
||||
err.maxSize = MAX_LITERAL_SIZE;
|
||||
this.emit('error', err);
|
||||
return false;
|
||||
}
|
||||
|
||||
this.state = LITERAL;
|
||||
this.literalWaiting = literalSize;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async processInputChunk(chunk, startPos) {
|
||||
startPos = startPos || 0;
|
||||
if (startPos >= chunk.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (this.state) {
|
||||
case LINE: {
|
||||
let lineStart = startPos;
|
||||
for (let i = startPos, len = chunk.length; i < len; i++) {
|
||||
if (chunk[i] === LF) {
|
||||
// line end found
|
||||
this.lineBuffer.push(chunk.slice(lineStart, i + 1));
|
||||
lineStart = i + 1;
|
||||
|
||||
let line = Buffer.concat(this.lineBuffer);
|
||||
|
||||
this.inputBuffer.push(line);
|
||||
this.lineBuffer = [];
|
||||
|
||||
// try to detect if this is a literal start
|
||||
if (this.checkLiteralMarker(line)) {
|
||||
// switch into line mode and start over
|
||||
return await this.processInputChunk(chunk, lineStart);
|
||||
}
|
||||
|
||||
// reached end of command input, emit it
|
||||
let payload = this.inputBuffer.length === 1 ? this.inputBuffer[0] : Buffer.concat(this.inputBuffer);
|
||||
let literals = this.literals;
|
||||
this.inputBuffer = [];
|
||||
this.literals = [];
|
||||
|
||||
if (payload.length) {
|
||||
// remove final line terminator
|
||||
let skipBytes = 0;
|
||||
if (payload.length >= 1 && payload[payload.length - 1] === LF) {
|
||||
skipBytes++;
|
||||
if (payload.length >= 2 && payload[payload.length - 2] === CR) {
|
||||
skipBytes++;
|
||||
}
|
||||
}
|
||||
|
||||
if (skipBytes) {
|
||||
payload = payload.slice(0, payload.length - skipBytes);
|
||||
}
|
||||
|
||||
if (payload.length) {
|
||||
await new Promise(resolve => {
|
||||
this.push({ payload, literals, next: resolve });
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (lineStart < chunk.length) {
|
||||
this.lineBuffer.push(chunk.slice(lineStart));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case LITERAL: {
|
||||
// exactly until end of chunk
|
||||
if (chunk.length === startPos + this.literalWaiting) {
|
||||
if (!startPos) {
|
||||
this.literalBuffer.push(chunk);
|
||||
} else {
|
||||
this.literalBuffer.push(chunk.slice(startPos));
|
||||
}
|
||||
|
||||
this.literalWaiting -= chunk.length;
|
||||
this.literals.push(Buffer.concat(this.literalBuffer));
|
||||
this.literalBuffer = [];
|
||||
this.state = LINE;
|
||||
|
||||
return;
|
||||
} else if (chunk.length > startPos + this.literalWaiting) {
|
||||
let partial = chunk.slice(startPos, startPos + this.literalWaiting);
|
||||
this.literalBuffer.push(partial);
|
||||
startPos += partial.length;
|
||||
this.literalWaiting -= partial.length;
|
||||
this.literals.push(Buffer.concat(this.literalBuffer));
|
||||
this.literalBuffer = [];
|
||||
this.state = LINE;
|
||||
|
||||
return await this.processInputChunk(chunk, startPos);
|
||||
} else {
|
||||
let partial = chunk.slice(startPos);
|
||||
this.literalBuffer.push(partial);
|
||||
startPos += partial.length;
|
||||
this.literalWaiting -= partial.length;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async processInput() {
|
||||
let data;
|
||||
let processedCount = 0;
|
||||
while ((data = this.inputQueue.shift())) {
|
||||
await this.processInputChunk(data.chunk);
|
||||
// mark chunk as processed
|
||||
data.next();
|
||||
|
||||
// Yield to event loop every 10 chunks to prevent CPU blocking
|
||||
processedCount++;
|
||||
if (processedCount % 10 === 0) {
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_transform(chunk, encoding, next) {
|
||||
if (typeof chunk === 'string') {
|
||||
chunk = Buffer.from(chunk, encoding);
|
||||
}
|
||||
|
||||
if (!chunk || !chunk.length) {
|
||||
return next();
|
||||
}
|
||||
|
||||
this.readBytesCounter += chunk.length;
|
||||
|
||||
if (this.options.logRaw) {
|
||||
this.log.trace({
|
||||
src: 's',
|
||||
msg: 'read from socket',
|
||||
data: chunk.toString('base64'),
|
||||
compress: !!this.compress,
|
||||
secure: !!this.secureConnection,
|
||||
cid: this.cid
|
||||
});
|
||||
}
|
||||
|
||||
if (chunk && chunk.length) {
|
||||
this.inputQueue.push({ chunk, next });
|
||||
}
|
||||
|
||||
if (!this.processingInput) {
|
||||
this.processingInput = true;
|
||||
this.processInput()
|
||||
.catch(err => this.emit('error', err))
|
||||
.finally(() => (this.processingInput = false));
|
||||
}
|
||||
}
|
||||
|
||||
_flush(next) {
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.ImapStream = ImapStream;
|
||||
165
backend/node_modules/imapflow/lib/handler/parser-instance.js
generated
vendored
Normal file
165
backend/node_modules/imapflow/lib/handler/parser-instance.js
generated
vendored
Normal file
@@ -0,0 +1,165 @@
|
||||
/* eslint new-cap: 0 */
|
||||
|
||||
'use strict';
|
||||
|
||||
const imapFormalSyntax = require('./imap-formal-syntax');
|
||||
|
||||
const { TokenParser } = require('./token-parser');
|
||||
|
||||
class ParserInstance {
|
||||
constructor(input, options) {
|
||||
this.input = (input || '').toString();
|
||||
this.options = options || {};
|
||||
this.remainder = this.input;
|
||||
this.pos = 0;
|
||||
}
|
||||
|
||||
async getTag() {
|
||||
if (!this.tag) {
|
||||
this.tag = await this.getElement(imapFormalSyntax.tag() + '*+', true);
|
||||
}
|
||||
return this.tag;
|
||||
}
|
||||
|
||||
async getCommand() {
|
||||
if (this.tag === '+') {
|
||||
// special case
|
||||
this.humanReadable = this.remainder.trim();
|
||||
this.remainder = '';
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!this.command) {
|
||||
this.command = await this.getElement(imapFormalSyntax.command());
|
||||
}
|
||||
|
||||
switch ((this.command || '').toString().toUpperCase()) {
|
||||
case 'OK':
|
||||
case 'NO':
|
||||
case 'BAD':
|
||||
case 'PREAUTH':
|
||||
case 'BYE':
|
||||
{
|
||||
let match = this.remainder.match(/^\s+\[/);
|
||||
if (match) {
|
||||
let nesting = 1;
|
||||
for (let i = match[0].length; i <= this.remainder.length; i++) {
|
||||
let c = this.remainder[i];
|
||||
|
||||
if (c === '[') {
|
||||
nesting++;
|
||||
} else if (c === ']') {
|
||||
nesting--;
|
||||
}
|
||||
if (!nesting) {
|
||||
this.humanReadable = this.remainder.substring(i + 1).trim();
|
||||
this.remainder = this.remainder.substring(0, i + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.humanReadable = this.remainder.trim();
|
||||
this.remainder = '';
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return this.command;
|
||||
}
|
||||
|
||||
async getElement(syntax) {
|
||||
let match, element, errPos;
|
||||
|
||||
if (this.remainder.match(/^\s/)) {
|
||||
let error = new Error(`Unexpected whitespace at position ${this.pos} [E1]`);
|
||||
error.code = 'ParserError1';
|
||||
error.parserContext = { input: this.input, pos: this.pos };
|
||||
throw error;
|
||||
}
|
||||
|
||||
if ((match = this.remainder.match(/^\s*[^\s]+(?=\s|$)/))) {
|
||||
element = match[0];
|
||||
if ((errPos = imapFormalSyntax.verify(element, syntax)) >= 0) {
|
||||
if (this.tag === 'Server' && element === 'Unavailable.') {
|
||||
// Exchange error
|
||||
let error = new Error(`Server returned an error: ${this.input}`);
|
||||
error.code = 'ParserErrorExchange';
|
||||
error.parserContext = {
|
||||
input: this.input,
|
||||
element,
|
||||
pos: this.pos,
|
||||
value: {
|
||||
tag: '*',
|
||||
command: 'BAD',
|
||||
attributes: [{ type: 'TEXT', value: this.input }]
|
||||
}
|
||||
};
|
||||
throw error;
|
||||
}
|
||||
|
||||
let error = new Error(`Unexpected char at position ${this.pos + errPos} [E2: ${JSON.stringify(element.charAt(errPos))}]`);
|
||||
error.code = 'ParserError2';
|
||||
error.parserContext = { input: this.input, element, pos: this.pos };
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
let error = new Error(`Unexpected end of input at position ${this.pos} [E3]`);
|
||||
error.code = 'ParserError3';
|
||||
error.parserContext = { input: this.input, pos: this.pos };
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.pos += match[0].length;
|
||||
this.remainder = this.remainder.substr(match[0].length);
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
async getSpace() {
|
||||
if (!this.remainder.length) {
|
||||
if (this.tag === '+' && this.pos === 1) {
|
||||
// special case, empty + response
|
||||
return;
|
||||
}
|
||||
|
||||
let error = new Error(`Unexpected end of input at position ${this.pos} [E4]`);
|
||||
error.code = 'ParserError4';
|
||||
error.parserContext = { input: this.input, pos: this.pos };
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (imapFormalSyntax.verify(this.remainder.charAt(0), imapFormalSyntax.SP()) >= 0) {
|
||||
let error = new Error(`Unexpected char at position ${this.pos} [E5: ${JSON.stringify(this.remainder.charAt(0))}]`);
|
||||
error.code = 'ParserError5';
|
||||
error.parserContext = { input: this.input, element: this.remainder, pos: this.pos };
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.pos++;
|
||||
this.remainder = this.remainder.substr(1);
|
||||
}
|
||||
|
||||
async getAttributes() {
|
||||
if (!this.remainder.length) {
|
||||
let error = new Error(`Unexpected end of input at position ${this.pos} [E6]`);
|
||||
error.code = 'ParserError6';
|
||||
error.parserContext = { input: this.input, pos: this.pos };
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (this.remainder.match(/^\s/)) {
|
||||
let error = new Error(`Unexpected whitespace at position ${this.pos} [E7]`);
|
||||
error.code = 'ParserError7';
|
||||
error.parserContext = { input: this.input, element: this.remainder, pos: this.pos };
|
||||
throw error;
|
||||
}
|
||||
|
||||
const tokenParser = new TokenParser(this, this.pos, this.remainder, this.options);
|
||||
|
||||
return await tokenParser.getAttributes();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.ParserInstance = ParserInstance;
|
||||
654
backend/node_modules/imapflow/lib/handler/token-parser.js
generated
vendored
Normal file
654
backend/node_modules/imapflow/lib/handler/token-parser.js
generated
vendored
Normal file
@@ -0,0 +1,654 @@
|
||||
/* eslint new-cap: 0 */
|
||||
|
||||
'use strict';
|
||||
|
||||
const imapFormalSyntax = require('./imap-formal-syntax');
|
||||
|
||||
const STATE_ATOM = 0x001;
|
||||
const STATE_LITERAL = 0x002;
|
||||
const STATE_NORMAL = 0x003;
|
||||
const STATE_PARTIAL = 0x004;
|
||||
const STATE_SEQUENCE = 0x005;
|
||||
const STATE_STRING = 0x006;
|
||||
const STATE_TEXT = 0x007;
|
||||
|
||||
const RE_DIGITS = /^\d+$/;
|
||||
const RE_SINGLE_DIGIT = /^\d$/;
|
||||
|
||||
const MAX_NODE_DEPTH = 25;
|
||||
|
||||
class TokenParser {
|
||||
constructor(parent, startPos, str, options) {
|
||||
this.str = (str || '').toString();
|
||||
this.options = options || {};
|
||||
this.parent = parent;
|
||||
|
||||
this.tree = this.currentNode = this.createNode();
|
||||
this.pos = startPos || 0;
|
||||
|
||||
this.currentNode.type = 'TREE';
|
||||
|
||||
this.state = STATE_NORMAL;
|
||||
}
|
||||
|
||||
async getAttributes() {
|
||||
await this.processString();
|
||||
|
||||
const attributes = [];
|
||||
let branch = attributes;
|
||||
|
||||
let walk = async node => {
|
||||
let curBranch = branch;
|
||||
let elm;
|
||||
let partial;
|
||||
|
||||
if (!node.isClosed && node.type === 'SEQUENCE' && node.value === '*') {
|
||||
node.isClosed = true;
|
||||
node.type = 'ATOM';
|
||||
}
|
||||
|
||||
// If the node was never closed, throw it
|
||||
if (!node.isClosed) {
|
||||
let error = new Error(`Unexpected end of input at position ${this.pos + this.str.length - 1} [E9]`);
|
||||
error.code = 'ParserError9';
|
||||
error.parserContext = { input: this.str, pos: this.pos + this.str.length - 1 };
|
||||
throw error;
|
||||
}
|
||||
|
||||
let type = (node.type || '').toString().toUpperCase();
|
||||
|
||||
switch (type) {
|
||||
case 'LITERAL':
|
||||
case 'STRING':
|
||||
case 'SEQUENCE':
|
||||
elm = {
|
||||
type: node.type.toUpperCase(),
|
||||
value: node.value
|
||||
};
|
||||
branch.push(elm);
|
||||
break;
|
||||
|
||||
case 'ATOM':
|
||||
if (node.value.toUpperCase() === 'NIL') {
|
||||
branch.push(null);
|
||||
break;
|
||||
}
|
||||
elm = {
|
||||
type: node.type.toUpperCase(),
|
||||
value: node.value
|
||||
};
|
||||
branch.push(elm);
|
||||
break;
|
||||
|
||||
case 'SECTION':
|
||||
branch = branch[branch.length - 1].section = [];
|
||||
break;
|
||||
|
||||
case 'LIST':
|
||||
elm = [];
|
||||
branch.push(elm);
|
||||
branch = elm;
|
||||
break;
|
||||
|
||||
case 'PARTIAL':
|
||||
partial = node.value.split('.').map(Number);
|
||||
branch[branch.length - 1].partial = partial;
|
||||
break;
|
||||
}
|
||||
|
||||
for (let childNode of node.childNodes) {
|
||||
await walk(childNode);
|
||||
}
|
||||
|
||||
branch = curBranch;
|
||||
};
|
||||
|
||||
await walk(this.tree);
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
createNode(parentNode, startPos) {
|
||||
let node = {
|
||||
childNodes: [],
|
||||
type: false,
|
||||
value: '',
|
||||
isClosed: true
|
||||
};
|
||||
|
||||
if (parentNode) {
|
||||
node.parentNode = parentNode;
|
||||
node.depth = parentNode.depth + 1;
|
||||
} else {
|
||||
node.depth = 0;
|
||||
}
|
||||
|
||||
if (node.depth > MAX_NODE_DEPTH) {
|
||||
let error = new Error('Too much nesting in IMAP string');
|
||||
error.code = 'MAX_IMAP_NESTING_REACHED';
|
||||
error._imapStr = this.str;
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (typeof startPos === 'number') {
|
||||
node.startPos = startPos;
|
||||
}
|
||||
|
||||
if (parentNode) {
|
||||
parentNode.childNodes.push(node);
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
async processString() {
|
||||
let chr, i, len;
|
||||
|
||||
const checkSP = () => {
|
||||
// jump to the next non whitespace pos
|
||||
while (this.str.charAt(i + 1) === ' ') {
|
||||
i++;
|
||||
}
|
||||
};
|
||||
|
||||
for (i = 0, len = this.str.length; i < len; i++) {
|
||||
chr = this.str.charAt(i);
|
||||
|
||||
switch (this.state) {
|
||||
case STATE_NORMAL:
|
||||
switch (chr) {
|
||||
// DQUOTE starts a new string
|
||||
case '"':
|
||||
this.currentNode = this.createNode(this.currentNode, this.pos + i);
|
||||
this.currentNode.type = 'string';
|
||||
this.state = STATE_STRING;
|
||||
this.currentNode.isClosed = false;
|
||||
break;
|
||||
|
||||
// ( starts a new list
|
||||
case '(':
|
||||
this.currentNode = this.createNode(this.currentNode, this.pos + i);
|
||||
this.currentNode.type = 'LIST';
|
||||
this.currentNode.isClosed = false;
|
||||
break;
|
||||
|
||||
// ) closes a list
|
||||
case ')':
|
||||
if (this.currentNode.type !== 'LIST') {
|
||||
let error = new Error(`Unexpected list terminator ) at position ${this.pos + i} [E10]`);
|
||||
error.code = 'ParserError10';
|
||||
error.parserContext = { input: this.str, pos: this.pos + i, chr };
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.currentNode.isClosed = true;
|
||||
this.currentNode.endPos = this.pos + i;
|
||||
this.currentNode = this.currentNode.parentNode;
|
||||
|
||||
checkSP();
|
||||
break;
|
||||
|
||||
// ] closes section group
|
||||
case ']':
|
||||
if (this.currentNode.type !== 'SECTION') {
|
||||
let error = new Error(`Unexpected section terminator ] at position ${this.pos + i} [E11]`);
|
||||
error.code = 'ParserError11';
|
||||
error.parserContext = { input: this.str, pos: this.pos + i, chr };
|
||||
throw error;
|
||||
}
|
||||
this.currentNode.isClosed = true;
|
||||
this.currentNode.endPos = this.pos + i;
|
||||
this.currentNode = this.currentNode.parentNode;
|
||||
|
||||
checkSP();
|
||||
break;
|
||||
|
||||
// < starts a new partial
|
||||
case '<':
|
||||
if (this.str.charAt(i - 1) !== ']') {
|
||||
this.currentNode = this.createNode(this.currentNode, this.pos + i);
|
||||
this.currentNode.type = 'ATOM';
|
||||
this.currentNode.value = chr;
|
||||
this.state = STATE_ATOM;
|
||||
} else {
|
||||
this.currentNode = this.createNode(this.currentNode, this.pos + i);
|
||||
this.currentNode.type = 'PARTIAL';
|
||||
this.state = STATE_PARTIAL;
|
||||
this.currentNode.isClosed = false;
|
||||
}
|
||||
break;
|
||||
|
||||
// binary literal8
|
||||
case '~': {
|
||||
let nextChr = this.str.charAt(i + 1);
|
||||
if (nextChr !== '{') {
|
||||
if (imapFormalSyntax['ATOM-CHAR']().indexOf(nextChr) >= 0) {
|
||||
// treat as ATOM
|
||||
this.currentNode = this.createNode(this.currentNode, this.pos + i);
|
||||
this.currentNode.type = 'ATOM';
|
||||
this.currentNode.value = chr;
|
||||
this.state = STATE_ATOM;
|
||||
break;
|
||||
}
|
||||
|
||||
let error = new Error(`Unexpected literal8 marker at position ${this.pos + i} [E12]`);
|
||||
error.code = 'ParserError12';
|
||||
error.parserContext = { input: this.str, pos: this.pos + i, chr };
|
||||
throw error;
|
||||
}
|
||||
this.expectedLiteralType = 'literal8';
|
||||
break;
|
||||
}
|
||||
|
||||
// { starts a new literal
|
||||
case '{':
|
||||
this.currentNode = this.createNode(this.currentNode, this.pos + i);
|
||||
this.currentNode.type = 'LITERAL';
|
||||
this.currentNode.literalType = this.expectedLiteralType || 'literal';
|
||||
this.expectedLiteralType = false;
|
||||
this.state = STATE_LITERAL;
|
||||
this.currentNode.isClosed = false;
|
||||
break;
|
||||
|
||||
// * starts a new sequence
|
||||
case '*':
|
||||
this.currentNode = this.createNode(this.currentNode, this.pos + i);
|
||||
this.currentNode.type = 'SEQUENCE';
|
||||
this.currentNode.value = chr;
|
||||
this.currentNode.isClosed = false;
|
||||
this.state = STATE_SEQUENCE;
|
||||
break;
|
||||
|
||||
// normally a space should never occur
|
||||
case ' ':
|
||||
// just ignore
|
||||
break;
|
||||
|
||||
// [ starts section
|
||||
case '[':
|
||||
// If it is the *first* element after response command, then process as a response argument list
|
||||
if (['OK', 'NO', 'BAD', 'BYE', 'PREAUTH'].includes(this.parent.command.toUpperCase()) && this.currentNode === this.tree) {
|
||||
this.currentNode.endPos = this.pos + i;
|
||||
|
||||
this.currentNode = this.createNode(this.currentNode, this.pos + i);
|
||||
this.currentNode.type = 'ATOM';
|
||||
|
||||
this.currentNode = this.createNode(this.currentNode, this.pos + i);
|
||||
this.currentNode.type = 'SECTION';
|
||||
this.currentNode.isClosed = false;
|
||||
this.state = STATE_NORMAL;
|
||||
|
||||
// RFC2221 defines a response code REFERRAL whose payload is an
|
||||
// RFC2192/RFC5092 imapurl that we will try to parse as an ATOM but
|
||||
// fail quite badly at parsing. Since the imapurl is such a unique
|
||||
// (and crazy) term, we just specialize that case here.
|
||||
if (this.str.substr(i + 1, 9).toUpperCase() === 'REFERRAL ') {
|
||||
// create the REFERRAL atom
|
||||
this.currentNode = this.createNode(this.currentNode, this.pos + i + 1);
|
||||
this.currentNode.type = 'ATOM';
|
||||
this.currentNode.endPos = this.pos + i + 8;
|
||||
this.currentNode.value = 'REFERRAL';
|
||||
this.currentNode = this.currentNode.parentNode;
|
||||
|
||||
// eat all the way through the ] to be the IMAPURL token.
|
||||
this.currentNode = this.createNode(this.currentNode, this.pos + i + 10);
|
||||
// just call this an ATOM, even though IMAPURL might be more correct
|
||||
this.currentNode.type = 'ATOM';
|
||||
// jump i to the ']'
|
||||
i = this.str.indexOf(']', i + 10);
|
||||
this.currentNode.endPos = this.pos + i - 1;
|
||||
this.currentNode.value = this.str.substring(this.currentNode.startPos - this.pos, this.currentNode.endPos - this.pos + 1);
|
||||
this.currentNode = this.currentNode.parentNode;
|
||||
|
||||
// close out the SECTION
|
||||
this.currentNode.isClosed = true;
|
||||
this.currentNode = this.currentNode.parentNode;
|
||||
|
||||
checkSP();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
/* falls through */
|
||||
default:
|
||||
// Any ATOM supported char starts a new Atom sequence, otherwise throw an error
|
||||
// Allow \ as the first char for atom to support system flags
|
||||
// Allow % to support LIST '' %
|
||||
// Allow 8bit characters (presumably unicode)
|
||||
if (imapFormalSyntax['ATOM-CHAR']().indexOf(chr) < 0 && chr !== '\\' && chr !== '%' && chr.charCodeAt(0) < 0x80) {
|
||||
let error = new Error(`Unexpected char at position ${this.pos + i} [E13: ${JSON.stringify(chr)}]`);
|
||||
error.code = 'ParserError13';
|
||||
error.parserContext = { input: this.str, pos: this.pos + i, chr };
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.currentNode = this.createNode(this.currentNode, this.pos + i);
|
||||
this.currentNode.type = 'ATOM';
|
||||
this.currentNode.value = chr;
|
||||
this.state = STATE_ATOM;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case STATE_ATOM:
|
||||
// space finishes an atom
|
||||
if (chr === ' ') {
|
||||
this.currentNode.endPos = this.pos + i - 1;
|
||||
this.currentNode = this.currentNode.parentNode;
|
||||
this.state = STATE_NORMAL;
|
||||
break;
|
||||
}
|
||||
|
||||
//
|
||||
if (
|
||||
this.currentNode.parentNode &&
|
||||
((chr === ')' && this.currentNode.parentNode.type === 'LIST') || (chr === ']' && this.currentNode.parentNode.type === 'SECTION'))
|
||||
) {
|
||||
this.currentNode.endPos = this.pos + i - 1;
|
||||
this.currentNode = this.currentNode.parentNode;
|
||||
|
||||
this.currentNode.isClosed = true;
|
||||
this.currentNode.endPos = this.pos + i;
|
||||
this.currentNode = this.currentNode.parentNode;
|
||||
this.state = STATE_NORMAL;
|
||||
checkSP();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if ((chr === ',' || chr === ':') && RE_DIGITS.test(this.currentNode.value)) {
|
||||
this.currentNode.type = 'SEQUENCE';
|
||||
this.currentNode.isClosed = true;
|
||||
this.state = STATE_SEQUENCE;
|
||||
}
|
||||
|
||||
// [ starts a section group for this element
|
||||
// Allowed only for selected elements, otherwise falls through to regular ATOM processing
|
||||
if (chr === '[' && ['BODY', 'BODY.PEEK', 'BINARY', 'BINARY.PEEK'].indexOf(this.currentNode.value.toUpperCase()) >= 0) {
|
||||
this.currentNode.endPos = this.pos + i;
|
||||
this.currentNode = this.createNode(this.currentNode.parentNode, this.pos + i);
|
||||
this.currentNode.type = 'SECTION';
|
||||
this.currentNode.isClosed = false;
|
||||
this.state = STATE_NORMAL;
|
||||
break;
|
||||
}
|
||||
|
||||
// if the char is not ATOM compatible, throw. Allow \* as an exception
|
||||
if (
|
||||
imapFormalSyntax['ATOM-CHAR']().indexOf(chr) < 0 &&
|
||||
chr.charCodeAt(0) < 0x80 && // allow 8bit (presumably unicode) bytes
|
||||
chr !== ']' &&
|
||||
!(chr === '*' && this.currentNode.value === '\\') &&
|
||||
(!this.parent || !this.parent.command || !['NO', 'BAD', 'OK'].includes(this.parent.command))
|
||||
) {
|
||||
let error = new Error(`Unexpected char at position ${this.pos + i} [E16: ${JSON.stringify(chr)}]`);
|
||||
error.code = 'ParserError16';
|
||||
error.parserContext = { input: this.str, pos: this.pos + i, chr };
|
||||
throw error;
|
||||
} else if (this.currentNode.value === '\\*') {
|
||||
let error = new Error(`Unexpected char at position ${this.pos + i} [E17: ${JSON.stringify(chr)}]`);
|
||||
error.code = 'ParserError17';
|
||||
error.parserContext = { input: this.str, pos: this.pos + i, chr };
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.currentNode.value += chr;
|
||||
break;
|
||||
|
||||
case STATE_STRING:
|
||||
// DQUOTE ends the string sequence
|
||||
if (chr === '"') {
|
||||
this.currentNode.endPos = this.pos + i;
|
||||
this.currentNode.isClosed = true;
|
||||
this.currentNode = this.currentNode.parentNode;
|
||||
this.state = STATE_NORMAL;
|
||||
|
||||
checkSP();
|
||||
break;
|
||||
}
|
||||
|
||||
// \ Escapes the following char
|
||||
if (chr === '\\') {
|
||||
i++;
|
||||
if (i >= len) {
|
||||
let error = new Error(`Unexpected end of input at position ${this.pos + i} [E18]`);
|
||||
error.code = 'ParserError18';
|
||||
error.parserContext = { input: this.str, pos: this.pos + i };
|
||||
throw error;
|
||||
}
|
||||
chr = this.str.charAt(i);
|
||||
}
|
||||
|
||||
this.currentNode.value += chr;
|
||||
break;
|
||||
|
||||
case STATE_PARTIAL:
|
||||
if (chr === '>') {
|
||||
if (this.currentNode.value.at(-1) === '.') {
|
||||
let error = new Error(`Unexpected end of partial at position ${this.pos + i} [E19]`);
|
||||
error.code = 'ParserError19';
|
||||
error.parserContext = { input: this.str, pos: this.pos + i, chr };
|
||||
throw error;
|
||||
}
|
||||
this.currentNode.endPos = this.pos + i;
|
||||
this.currentNode.isClosed = true;
|
||||
this.currentNode = this.currentNode.parentNode;
|
||||
this.state = STATE_NORMAL;
|
||||
checkSP();
|
||||
break;
|
||||
}
|
||||
|
||||
if (chr === '.' && (!this.currentNode.value.length || this.currentNode.value.match(/\./))) {
|
||||
let error = new Error(`Unexpected partial separator . at position ${this.pos + i} [E20]`);
|
||||
error.code = 'ParserError20';
|
||||
error.parserContext = { input: this.str, pos: this.pos + i, chr };
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (imapFormalSyntax.DIGIT().indexOf(chr) < 0 && chr !== '.') {
|
||||
let error = new Error(`Unexpected char at position ${this.pos + i} [E21: ${JSON.stringify(chr)}]`);
|
||||
error.code = 'ParserError21';
|
||||
error.parserContext = { input: this.str, pos: this.pos + i, chr };
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (this.currentNode.value.match(/^0$|\.0$/) && chr !== '.') {
|
||||
let error = new Error(`Invalid partial at position ${this.pos + i} [E22: ${JSON.stringify(chr)}]`);
|
||||
error.code = 'ParserError22';
|
||||
error.parserContext = { input: this.str, pos: this.pos + i, chr };
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.currentNode.value += chr;
|
||||
break;
|
||||
|
||||
case STATE_LITERAL:
|
||||
if (this.currentNode.started) {
|
||||
// only relevant if literals are not already parsed out from input
|
||||
|
||||
// Disabled NULL byte check
|
||||
// See https://github.com/emailjs/emailjs-imap-handler/commit/f11b2822bedabe492236e8263afc630134a3c41c
|
||||
/*
|
||||
if (chr === '\u0000') {
|
||||
throw new Error('Unexpected \\x00 at position ' + (this.pos + i));
|
||||
}
|
||||
*/
|
||||
|
||||
this.currentNode.chBuffer[this.currentNode.chPos++] = chr.charCodeAt(0);
|
||||
|
||||
if (this.currentNode.chPos >= this.currentNode.literalLength) {
|
||||
this.currentNode.endPos = this.pos + i;
|
||||
this.currentNode.isClosed = true;
|
||||
this.currentNode.value = this.currentNode.chBuffer.toString('binary');
|
||||
this.currentNode.chBuffer = Buffer.alloc(0);
|
||||
this.currentNode = this.currentNode.parentNode;
|
||||
this.state = STATE_NORMAL;
|
||||
checkSP();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (chr === '+' && this.options.literalPlus) {
|
||||
this.currentNode.literalPlus = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (chr === '}') {
|
||||
if (!('literalLength' in this.currentNode)) {
|
||||
let error = new Error(`Unexpected literal prefix end char } at position ${this.pos + i} [E23]`);
|
||||
error.code = 'ParserError23';
|
||||
error.parserContext = { input: this.str, pos: this.pos + i, chr };
|
||||
throw error;
|
||||
}
|
||||
if (this.str.charAt(i + 1) === '\n') {
|
||||
i++;
|
||||
} else if (this.str.charAt(i + 1) === '\r' && this.str.charAt(i + 2) === '\n') {
|
||||
i += 2;
|
||||
} else {
|
||||
let error = new Error(`Unexpected char at position ${this.pos + i} [E24: ${JSON.stringify(chr)}]`);
|
||||
error.code = 'ParserError24';
|
||||
error.parserContext = { input: this.str, pos: this.pos + i, chr };
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.currentNode.literalLength = Number(this.currentNode.literalLength);
|
||||
|
||||
if (!this.currentNode.literalLength) {
|
||||
// special case where literal content length is 0
|
||||
// close the node right away, do not wait for additional input
|
||||
this.currentNode.endPos = this.pos + i;
|
||||
this.currentNode.isClosed = true;
|
||||
this.currentNode = this.currentNode.parentNode;
|
||||
this.state = STATE_NORMAL;
|
||||
checkSP();
|
||||
} else if (this.options.literals) {
|
||||
// use the next precached literal values
|
||||
this.currentNode.value = this.options.literals.shift();
|
||||
|
||||
// only APPEND arguments are kept as Buffers
|
||||
/*
|
||||
if ((this.parent.command || '').toString().toUpperCase() !== 'APPEND') {
|
||||
this.currentNode.value = this.currentNode.value.toString('binary');
|
||||
}
|
||||
*/
|
||||
|
||||
this.currentNode.endPos = this.pos + i + this.currentNode.value.length;
|
||||
|
||||
this.currentNode.started = false;
|
||||
this.currentNode.isClosed = true;
|
||||
this.currentNode = this.currentNode.parentNode;
|
||||
this.state = STATE_NORMAL;
|
||||
checkSP();
|
||||
} else {
|
||||
this.currentNode.started = true;
|
||||
// Allocate expected size buffer. Max size check is already performed
|
||||
// Maybe should use allocUnsafe instead?
|
||||
this.currentNode.chBuffer = Buffer.alloc(this.currentNode.literalLength);
|
||||
this.currentNode.chPos = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (imapFormalSyntax.DIGIT().indexOf(chr) < 0) {
|
||||
let error = new Error(`Unexpected char at position ${this.pos + i} [E25: ${JSON.stringify(chr)}]`);
|
||||
error.code = 'ParserError25';
|
||||
error.parserContext = { input: this.str, pos: this.pos + i, chr };
|
||||
throw error;
|
||||
}
|
||||
if (this.currentNode.literalLength === '0') {
|
||||
let error = new Error(`Invalid literal at position ${this.pos + i} [E26]`);
|
||||
error.code = 'ParserError26';
|
||||
error.parserContext = { input: this.str, pos: this.pos + i, chr };
|
||||
throw error;
|
||||
}
|
||||
this.currentNode.literalLength = (this.currentNode.literalLength || '') + chr;
|
||||
break;
|
||||
|
||||
case STATE_SEQUENCE:
|
||||
// space finishes the sequence set
|
||||
if (chr === ' ') {
|
||||
if (!RE_SINGLE_DIGIT.test(this.currentNode.value.at(-1)) && this.currentNode.value.at(-1) !== '*') {
|
||||
let error = new Error(`Unexpected whitespace at position ${this.pos + i} [E27]`);
|
||||
error.code = 'ParserError27';
|
||||
error.parserContext = { input: this.str, pos: this.pos + i, chr };
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (this.currentNode.value !== '*' && this.currentNode.value.at(-1) === '*' && this.currentNode.value.at(-2) !== ':') {
|
||||
let error = new Error(`Unexpected whitespace at position ${this.pos + i} [E28]`);
|
||||
error.code = 'ParserError28';
|
||||
error.parserContext = { input: this.str, pos: this.pos + i, chr };
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.currentNode.isClosed = true;
|
||||
this.currentNode.endPos = this.pos + i - 1;
|
||||
this.currentNode = this.currentNode.parentNode;
|
||||
this.state = STATE_NORMAL;
|
||||
break;
|
||||
} else if (this.currentNode.parentNode && chr === ']' && this.currentNode.parentNode.type === 'SECTION') {
|
||||
this.currentNode.endPos = this.pos + i - 1;
|
||||
this.currentNode = this.currentNode.parentNode;
|
||||
|
||||
this.currentNode.isClosed = true;
|
||||
this.currentNode.endPos = this.pos + i;
|
||||
this.currentNode = this.currentNode.parentNode;
|
||||
this.state = STATE_NORMAL;
|
||||
|
||||
checkSP();
|
||||
break;
|
||||
}
|
||||
|
||||
if (chr === ':') {
|
||||
if (!RE_SINGLE_DIGIT.test(this.currentNode.value.at(-1)) && this.currentNode.value.at(-1) !== '*') {
|
||||
let error = new Error(`Unexpected range separator : at position ${this.pos + i} [E29]`);
|
||||
error.code = 'ParserError29';
|
||||
error.parserContext = { input: this.str, pos: this.pos + i, chr };
|
||||
throw error;
|
||||
}
|
||||
} else if (chr === '*') {
|
||||
if ([',', ':'].indexOf(this.currentNode.value.at(-1)) < 0) {
|
||||
let error = new Error(`Unexpected range wildcard at position ${this.pos + i} [E30]`);
|
||||
error.code = 'ParserError30';
|
||||
error.parserContext = { input: this.str, pos: this.pos + i, chr };
|
||||
throw error;
|
||||
}
|
||||
} else if (chr === ',') {
|
||||
if (!RE_SINGLE_DIGIT.test(this.currentNode.value.at(-1)) && this.currentNode.value.at(-1) !== '*') {
|
||||
let error = new Error(`Unexpected sequence separator , at position ${this.pos + i} [E31]`);
|
||||
error.code = 'ParserError31';
|
||||
error.parserContext = { input: this.str, pos: this.pos + i, chr };
|
||||
throw error;
|
||||
}
|
||||
if (this.currentNode.value.at(-1) === '*' && this.currentNode.value.at(-2) !== ':') {
|
||||
let error = new Error(`Unexpected sequence separator , at position ${this.pos + i} [E32]`);
|
||||
error.code = 'ParserError32';
|
||||
error.parserContext = { input: this.str, pos: this.pos + i, chr };
|
||||
throw error;
|
||||
}
|
||||
} else if (!RE_SINGLE_DIGIT.test(chr)) {
|
||||
let error = new Error(`Unexpected char at position ${this.pos + i} [E33: ${JSON.stringify(chr)}]`);
|
||||
error.code = 'ParserError33';
|
||||
error.parserContext = { input: this.str, pos: this.pos + i, chr };
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (RE_SINGLE_DIGIT.test(chr) && this.currentNode.value.at(-1) === '*') {
|
||||
let error = new Error(`Unexpected number at position ${this.pos + i} [E34: ${JSON.stringify(chr)}]`);
|
||||
error.code = 'ParserError34';
|
||||
error.parserContext = { input: this.str, pos: this.pos + i, chr };
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.currentNode.value += chr;
|
||||
break;
|
||||
|
||||
case STATE_TEXT:
|
||||
this.currentNode.value += chr;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.TokenParser = TokenParser;
|
||||
34
backend/node_modules/imapflow/lib/imap-commands.js
generated
vendored
Normal file
34
backend/node_modules/imapflow/lib/imap-commands.js
generated
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
/* eslint global-require:0 */
|
||||
|
||||
'use strict';
|
||||
|
||||
module.exports = new Map([
|
||||
['ID', require('./commands/id.js')],
|
||||
['CAPABILITY', require('./commands/capability.js')],
|
||||
['NAMESPACE', require('./commands/namespace.js')],
|
||||
['LOGIN', require('./commands/login.js')],
|
||||
['LOGOUT', require('./commands/logout.js')],
|
||||
['STARTTLS', require('./commands/starttls.js')],
|
||||
['LIST', require('./commands/list.js')],
|
||||
['ENABLE', require('./commands/enable.js')],
|
||||
['SELECT', require('./commands/select.js')],
|
||||
['FETCH', require('./commands/fetch.js')],
|
||||
['CREATE', require('./commands/create.js')],
|
||||
['DELETE', require('./commands/delete.js')],
|
||||
['RENAME', require('./commands/rename.js')],
|
||||
['CLOSE', require('./commands/close.js')],
|
||||
['SUBSCRIBE', require('./commands/subscribe.js')],
|
||||
['UNSUBSCRIBE', require('./commands/unsubscribe.js')],
|
||||
['STORE', require('./commands/store.js')],
|
||||
['SEARCH', require('./commands/search.js')],
|
||||
['NOOP', require('./commands/noop.js')],
|
||||
['EXPUNGE', require('./commands/expunge.js')],
|
||||
['APPEND', require('./commands/append.js')],
|
||||
['STATUS', require('./commands/status.js')],
|
||||
['COPY', require('./commands/copy.js')],
|
||||
['MOVE', require('./commands/move.js')],
|
||||
['COMPRESS', require('./commands/compress.js')],
|
||||
['QUOTA', require('./commands/quota.js')],
|
||||
['IDLE', require('./commands/idle.js')],
|
||||
['AUTHENTICATE', require('./commands/authenticate.js')]
|
||||
]);
|
||||
790
backend/node_modules/imapflow/lib/imap-flow.d.ts
generated
vendored
Normal file
790
backend/node_modules/imapflow/lib/imap-flow.d.ts
generated
vendored
Normal file
@@ -0,0 +1,790 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import { ConnectionOptions } from 'tls';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
export interface ImapFlowOptions {
|
||||
/** Hostname of the IMAP server */
|
||||
host: string;
|
||||
/** Port number for the IMAP server */
|
||||
port: number;
|
||||
/** If true, the connection will use TLS. If false, then TLS is used only if the server supports STARTTLS extension */
|
||||
secure?: boolean;
|
||||
/** Servername for SNI (or when host is set to an IP address) */
|
||||
servername?: string;
|
||||
/** If true, do not use COMPRESS=DEFLATE extension even if server supports it */
|
||||
disableCompression?: boolean;
|
||||
/** Authentication options */
|
||||
auth?: {
|
||||
/** Username */
|
||||
user: string;
|
||||
/** Password for regular authentication (if using OAuth2 then use `accessToken` instead) */
|
||||
pass?: string;
|
||||
/** OAuth2 access token, if using OAuth2 authentication */
|
||||
accessToken?: string;
|
||||
/** Optional login method override. Set to 'LOGIN', 'AUTH=LOGIN' or 'AUTH=PLAIN' to use specific method */
|
||||
loginMethod?: string;
|
||||
/** Authorization identity for SASL PLAIN (used for admin impersonation/delegation). When set, authenticates as `user` but authorizes as `authzid` */
|
||||
authzid?: string;
|
||||
};
|
||||
/** Client identification info sent to the server if server supports ID extension */
|
||||
clientInfo?: IdInfoObject;
|
||||
/** If true, then do not start IDLE when connection is established */
|
||||
disableAutoIdle?: boolean;
|
||||
/** Additional TLS options (see Node.js TLS documentation) */
|
||||
tls?: ConnectionOptions;
|
||||
/** Custom logger instance. Set to false to disable logging */
|
||||
logger?: Logger | false;
|
||||
/** If true, log data read and written to socket encoded in base64 */
|
||||
logRaw?: boolean;
|
||||
/** If true, emit 'log' events */
|
||||
emitLogs?: boolean;
|
||||
/** If true, then logs out automatically after successful authentication */
|
||||
verifyOnly?: boolean;
|
||||
/** If true and verifyOnly is set, lists mailboxes */
|
||||
includeMailboxes?: boolean;
|
||||
/** Proxy URL. Supports HTTP CONNECT (http:, https:) and SOCKS (socks:, socks4:, socks5:) proxies */
|
||||
proxy?: string;
|
||||
/** If true, then use QRESYNC instead of CONDSTORE. EXPUNGE notifications will include UID instead of sequence number */
|
||||
qresync?: boolean;
|
||||
/** If set, then breaks and restarts IDLE every maxIdleTime ms */
|
||||
maxIdleTime?: number;
|
||||
/** What command to run if IDLE is not supported. Defaults to 'NOOP' */
|
||||
missingIdleCommand?: 'NOOP' | 'SELECT' | 'STATUS';
|
||||
/** If true, ignores BINARY extension when making FETCH and APPEND calls */
|
||||
disableBinary?: boolean;
|
||||
/** If true, do not enable supported extensions */
|
||||
disableAutoEnable?: boolean;
|
||||
/** How long to wait for the connection to be established. Defaults to 90 seconds */
|
||||
connectionTimeout?: number;
|
||||
/** How long to wait for the greeting. Defaults to 16 seconds */
|
||||
greetingTimeout?: number;
|
||||
/** How long to wait for socket inactivity before timing out the connection. Defaults to 5 minutes */
|
||||
socketTimeout?: number;
|
||||
/** If true, uses TLS. If false, uses cleartext. If not set, upgrades to TLS if available */
|
||||
doSTARTTLS?: boolean;
|
||||
/** Custom instance ID string for logs */
|
||||
id?: string;
|
||||
/** Optional expunge event handler function */
|
||||
expungeHandler?: (event: ExpungeEvent) => Promise<void> | void;
|
||||
}
|
||||
|
||||
export interface Logger {
|
||||
debug(obj: any): void;
|
||||
info(obj: any): void;
|
||||
warn(obj: any): void;
|
||||
error(obj: any): void;
|
||||
}
|
||||
|
||||
export interface MailboxObject {
|
||||
/** Mailbox path */
|
||||
path: string;
|
||||
/** Mailbox path delimiter, usually "." or "/" */
|
||||
delimiter: string;
|
||||
/** List of flags for this mailbox */
|
||||
flags: Set<string>;
|
||||
/** One of special-use flags (if applicable): "\All", "\Archive", "\Drafts", "\Flagged", "\Junk", "\Sent", "\Trash". Additionally INBOX has non-standard "\Inbox" flag set */
|
||||
specialUse?: string;
|
||||
/** True if mailbox was found from the output of LIST command */
|
||||
listed?: boolean;
|
||||
/** True if mailbox was found from the output of LSUB command */
|
||||
subscribed?: boolean;
|
||||
/** A Set of flags available to use in this mailbox. If it is not set or includes special flag "\*" then any flag can be used */
|
||||
permanentFlags?: Set<string>;
|
||||
/** Unique mailbox ID if server has OBJECTID extension enabled */
|
||||
mailboxId?: string;
|
||||
/** Latest known modseq value if server has CONDSTORE or XYMHIGHESTMODSEQ enabled */
|
||||
highestModseq?: bigint;
|
||||
/** If true then the server doesn't support the persistent storage of mod-sequences for the mailbox */
|
||||
noModseq?: boolean;
|
||||
/** Mailbox UIDVALIDITY value */
|
||||
uidValidity: bigint;
|
||||
/** Next predicted UID */
|
||||
uidNext: number;
|
||||
/** Messages in this folder */
|
||||
exists: number;
|
||||
/** Read-only state */
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
export interface MailboxLockObject {
|
||||
/** Mailbox path */
|
||||
path: string;
|
||||
/** Release current lock */
|
||||
release(): void;
|
||||
}
|
||||
|
||||
export interface IdInfoObject {
|
||||
/** Name of the program */
|
||||
name?: string;
|
||||
/** Version number of the program */
|
||||
version?: string;
|
||||
/** Name of the operating system */
|
||||
os?: string;
|
||||
/** Vendor of the client/server */
|
||||
vendor?: string;
|
||||
/** URL to contact for support */
|
||||
'support-url'?: string;
|
||||
/** Date program was released */
|
||||
date?: Date;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface QuotaResponse {
|
||||
/** Mailbox path this quota applies to */
|
||||
path: string;
|
||||
/** Storage quota if provided by server */
|
||||
storage?: {
|
||||
/** Used storage in bytes */
|
||||
used: number;
|
||||
/** Total storage available */
|
||||
limit: number;
|
||||
};
|
||||
/** Message count quota if provided by server */
|
||||
messages?: {
|
||||
/** Stored messages */
|
||||
used: number;
|
||||
/** Maximum messages allowed */
|
||||
limit: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ListResponse {
|
||||
/** Mailbox path (unicode string) */
|
||||
path: string;
|
||||
/** Mailbox path as listed in the LIST/LSUB response */
|
||||
pathAsListed: string;
|
||||
/** Mailbox name (last part of path after delimiter) */
|
||||
name: string;
|
||||
/** Mailbox path delimiter, usually "." or "/" */
|
||||
delimiter: string;
|
||||
/** An array of parent folder names. All names are in unicode */
|
||||
parent: string[];
|
||||
/** Same as parent, but as a complete string path (unicode string) */
|
||||
parentPath: string;
|
||||
/** A set of flags for this mailbox */
|
||||
flags: Set<string>;
|
||||
/** One of special-use flags (if applicable) */
|
||||
specialUse?: string;
|
||||
/** True if mailbox was found from the output of LIST command */
|
||||
listed: boolean;
|
||||
/** True if mailbox was found from the output of LSUB command */
|
||||
subscribed: boolean;
|
||||
/** If statusQuery was used, then this value includes the status response */
|
||||
status?: StatusObject;
|
||||
}
|
||||
|
||||
export interface ListOptions {
|
||||
/** Request status items for every listed entry */
|
||||
statusQuery?: {
|
||||
/** If true request count of messages */
|
||||
messages?: boolean;
|
||||
/** If true request count of messages with \Recent tag */
|
||||
recent?: boolean;
|
||||
/** If true request predicted next UID */
|
||||
uidNext?: boolean;
|
||||
/** If true request mailbox UIDVALIDITY value */
|
||||
uidValidity?: boolean;
|
||||
/** If true request count of unseen messages */
|
||||
unseen?: boolean;
|
||||
/** If true request last known modseq value */
|
||||
highestModseq?: boolean;
|
||||
};
|
||||
/** Set specific paths as special use folders */
|
||||
specialUseHints?: {
|
||||
/** Path to "Sent Mail" folder */
|
||||
sent?: string;
|
||||
/** Path to "Trash" folder */
|
||||
trash?: string;
|
||||
/** Path to "Junk Mail" folder */
|
||||
junk?: string;
|
||||
/** Path to "Drafts" folder */
|
||||
drafts?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ListTreeResponse {
|
||||
/** If true then this is root node without any additional properties besides folders */
|
||||
root?: boolean;
|
||||
/** Mailbox path */
|
||||
path?: string;
|
||||
/** Mailbox name (last part of path after delimiter) */
|
||||
name?: string;
|
||||
/** Mailbox path delimiter, usually "." or "/" */
|
||||
delimiter?: string;
|
||||
/** List of flags for this mailbox */
|
||||
flags?: Set<string>;
|
||||
/** One of special-use flags (if applicable) */
|
||||
specialUse?: string;
|
||||
/** True if mailbox was found from the output of LIST command */
|
||||
listed?: boolean;
|
||||
/** True if mailbox was found from the output of LSUB command */
|
||||
subscribed?: boolean;
|
||||
/** If true then this mailbox can not be selected in the UI */
|
||||
disabled?: boolean;
|
||||
/** An array of subfolders */
|
||||
folders?: ListTreeResponse[];
|
||||
/** Status response */
|
||||
status?: StatusObject;
|
||||
}
|
||||
|
||||
export interface MailboxCreateResponse {
|
||||
/** Full mailbox path */
|
||||
path: string;
|
||||
/** Unique mailbox ID if server supports OBJECTID extension */
|
||||
mailboxId?: string;
|
||||
/** If true then mailbox was created otherwise it already existed */
|
||||
created: boolean;
|
||||
}
|
||||
|
||||
export interface MailboxRenameResponse {
|
||||
/** Full mailbox path that was renamed */
|
||||
path: string;
|
||||
/** New full mailbox path */
|
||||
newPath: string;
|
||||
}
|
||||
|
||||
export interface MailboxDeleteResponse {
|
||||
/** Full mailbox path that was deleted */
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface StatusObject {
|
||||
/** Full mailbox path that was checked */
|
||||
path: string;
|
||||
/** Count of messages */
|
||||
messages?: number;
|
||||
/** Count of messages with \Recent tag */
|
||||
recent?: number;
|
||||
/** Predicted next UID */
|
||||
uidNext?: number;
|
||||
/** Mailbox UIDVALIDITY value */
|
||||
uidValidity?: bigint;
|
||||
/** Count of unseen messages */
|
||||
unseen?: number;
|
||||
/** Last known modseq value (if CONDSTORE extension is enabled) */
|
||||
highestModseq?: bigint;
|
||||
}
|
||||
|
||||
export type SequenceString = string | number | bigint;
|
||||
|
||||
export interface SearchObject {
|
||||
/** Message ordering sequence range */
|
||||
seq?: SequenceString;
|
||||
/** Messages with (value is true) or without (value is false) \Answered flag */
|
||||
answered?: boolean;
|
||||
/** Messages with (value is true) or without (value is false) \Deleted flag */
|
||||
deleted?: boolean;
|
||||
/** Messages with (value is true) or without (value is false) \Draft flag */
|
||||
draft?: boolean;
|
||||
/** Messages with (value is true) or without (value is false) \Flagged flag */
|
||||
flagged?: boolean;
|
||||
/** Messages with (value is true) or without (value is false) \Seen flag */
|
||||
seen?: boolean;
|
||||
/** If true matches all messages */
|
||||
all?: boolean;
|
||||
/** If true matches messages that have the \Recent flag set but not the \Seen flag */
|
||||
new?: boolean;
|
||||
/** If true matches messages that do not have the \Recent flag set */
|
||||
old?: boolean;
|
||||
/** If true matches messages that have the \Recent flag set */
|
||||
recent?: boolean;
|
||||
/** Matches From: address field */
|
||||
from?: string;
|
||||
/** Matches To: address field */
|
||||
to?: string;
|
||||
/** Matches Cc: address field */
|
||||
cc?: string;
|
||||
/** Matches Bcc: address field */
|
||||
bcc?: string;
|
||||
/** Matches message body */
|
||||
body?: string;
|
||||
/** Matches message subject */
|
||||
subject?: string;
|
||||
/** Matches any text in headers and body */
|
||||
text?: string;
|
||||
/** Matches messages larger than value */
|
||||
larger?: number;
|
||||
/** Matches messages smaller than value */
|
||||
smaller?: number;
|
||||
/** UID sequence range */
|
||||
uid?: SequenceString;
|
||||
/** Matches messages with modseq higher than value */
|
||||
modseq?: bigint;
|
||||
/** Unique email ID. Only used if server supports OBJECTID or X-GM-EXT-1 extensions */
|
||||
emailId?: string;
|
||||
/** Unique thread ID. Only used if server supports OBJECTID or X-GM-EXT-1 extensions */
|
||||
threadId?: string;
|
||||
/** Matches messages received before date */
|
||||
before?: Date | string;
|
||||
/** Matches messages received on date (ignores time) */
|
||||
on?: Date | string;
|
||||
/** Matches messages received after date */
|
||||
since?: Date | string;
|
||||
/** Matches messages sent before date */
|
||||
sentBefore?: Date | string;
|
||||
/** Matches messages sent on date (ignores time) */
|
||||
sentOn?: Date | string;
|
||||
/** Matches messages sent after date */
|
||||
sentSince?: Date | string;
|
||||
/** Matches messages that have the custom flag set */
|
||||
keyword?: string;
|
||||
/** Matches messages that do not have the custom flag set */
|
||||
unKeyword?: string;
|
||||
/** Matches messages with header key set if value is true or messages where header partially matches a string value */
|
||||
header?: { [key: string]: boolean | string };
|
||||
/** A SearchObject object. It must not match */
|
||||
not?: SearchObject;
|
||||
/** An array of 2 or more SearchObject objects. At least one of these must match */
|
||||
or?: SearchObject[];
|
||||
/** Gmail raw search query (only for Gmail) */
|
||||
gmraw?: string;
|
||||
/** Gmail raw search query (alias for gmraw) */
|
||||
gmailraw?: string;
|
||||
}
|
||||
|
||||
export interface FetchQueryObject {
|
||||
/** If true then include UID in the response */
|
||||
uid?: boolean;
|
||||
/** If true then include flags Set in the response */
|
||||
flags?: boolean;
|
||||
/** If true then include parsed BODYSTRUCTURE object in the response */
|
||||
bodyStructure?: boolean;
|
||||
/** If true then include parsed ENVELOPE object in the response */
|
||||
envelope?: boolean;
|
||||
/** If true then include internal date value in the response */
|
||||
internalDate?: boolean;
|
||||
/** If true then include message size in the response */
|
||||
size?: boolean;
|
||||
/** If true then include full message in the response */
|
||||
source?: boolean | {
|
||||
/** Include full message in the response starting from start byte */
|
||||
start?: number;
|
||||
/** Include full message in the response, up to maxLength bytes */
|
||||
maxLength?: number;
|
||||
};
|
||||
/** If true then include thread ID in the response (only if server supports either OBJECTID or X-GM-EXT-1 extensions) */
|
||||
threadId?: boolean;
|
||||
/** If true then include GMail labels in the response (only if server supports X-GM-EXT-1 extension) */
|
||||
labels?: boolean;
|
||||
/** If true then includes full headers of the message in the response. If the value is an array of header keys then includes only headers listed in the array */
|
||||
headers?: boolean | string[];
|
||||
/** An array of BODYPART identifiers to include in the response */
|
||||
bodyParts?: Array<string | { key: string; start?: number; maxLength?: number }>;
|
||||
/** Fast macro equivalent to flags, internalDate, size */
|
||||
fast?: boolean;
|
||||
/** All macro equivalent to flags, internalDate, size, envelope */
|
||||
all?: boolean;
|
||||
/** Full macro equivalent to flags, internalDate, size, envelope, bodyStructure */
|
||||
full?: boolean;
|
||||
}
|
||||
|
||||
export interface MessageAddressObject {
|
||||
/** Name of the address object (unicode) */
|
||||
name?: string;
|
||||
/** Email address */
|
||||
address?: string;
|
||||
}
|
||||
|
||||
export interface MessageEnvelopeObject {
|
||||
/** Header date */
|
||||
date?: Date;
|
||||
/** Message subject (unicode) */
|
||||
subject?: string;
|
||||
/** Message ID of the message */
|
||||
messageId?: string;
|
||||
/** Message ID from In-Reply-To header */
|
||||
inReplyTo?: string;
|
||||
/** Array of addresses from the From: header */
|
||||
from?: MessageAddressObject[];
|
||||
/** Array of addresses from the Sender: header */
|
||||
sender?: MessageAddressObject[];
|
||||
/** Array of addresses from the Reply-To: header */
|
||||
replyTo?: MessageAddressObject[];
|
||||
/** Array of addresses from the To: header */
|
||||
to?: MessageAddressObject[];
|
||||
/** Array of addresses from the Cc: header */
|
||||
cc?: MessageAddressObject[];
|
||||
/** Array of addresses from the Bcc: header */
|
||||
bcc?: MessageAddressObject[];
|
||||
}
|
||||
|
||||
export interface MessageStructureObject {
|
||||
/** Body part number. This value can be used to later fetch the contents of this part of the message */
|
||||
part?: string;
|
||||
/** Content-Type of this node */
|
||||
type: string;
|
||||
/** Additional parameters for Content-Type, eg "charset" */
|
||||
parameters?: { [key: string]: string };
|
||||
/** Content-ID */
|
||||
id?: string;
|
||||
/** Transfer encoding */
|
||||
encoding?: string;
|
||||
/** Expected size of the node */
|
||||
size?: number;
|
||||
/** Message envelope of embedded RFC822 message */
|
||||
envelope?: MessageEnvelopeObject;
|
||||
/** Content disposition */
|
||||
disposition?: string;
|
||||
/** Additional parameters for Content-Disposition */
|
||||
dispositionParameters?: { [key: string]: string };
|
||||
/** An array of child nodes if this is a multipart node */
|
||||
childNodes?: MessageStructureObject[];
|
||||
/** MD5 hash */
|
||||
md5?: string;
|
||||
/** Language */
|
||||
language?: string[];
|
||||
/** Location */
|
||||
location?: string;
|
||||
/** Description */
|
||||
description?: string;
|
||||
/** Line count */
|
||||
lineCount?: number;
|
||||
}
|
||||
|
||||
export interface FetchMessageObject {
|
||||
/** Message sequence number. Always included in the response */
|
||||
seq: number;
|
||||
/** Message UID number. Always included in the response */
|
||||
uid: number;
|
||||
/** Message source for the requested byte range */
|
||||
source?: Buffer;
|
||||
/** Message Modseq number. Always included if the server supports CONDSTORE extension */
|
||||
modseq?: bigint;
|
||||
/** Unique email ID. Always included if server supports OBJECTID or X-GM-EXT-1 extensions */
|
||||
emailId?: string;
|
||||
/** Unique thread ID. Only present if server supports OBJECTID or X-GM-EXT-1 extension */
|
||||
threadId?: string;
|
||||
/** A Set of labels. Only present if server supports X-GM-EXT-1 extension */
|
||||
labels?: Set<string>;
|
||||
/** Message size */
|
||||
size?: number;
|
||||
/** A set of message flags */
|
||||
flags?: Set<string>;
|
||||
/** Flag color like "red", or "yellow". This value is derived from the flags Set */
|
||||
flagColor?: string;
|
||||
/** Message envelope */
|
||||
envelope?: MessageEnvelopeObject;
|
||||
/** Message body structure */
|
||||
bodyStructure?: MessageStructureObject;
|
||||
/** Message internal date */
|
||||
internalDate?: Date | string;
|
||||
/** A Map of message body parts where key is requested part identifier and value is a Buffer */
|
||||
bodyParts?: Map<string, Buffer>;
|
||||
/** Requested header lines as Buffer */
|
||||
headers?: Buffer;
|
||||
/** Account unique ID for this email */
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export interface DownloadObject {
|
||||
/** Content metadata */
|
||||
meta: {
|
||||
/** The fetch response size */
|
||||
expectedSize: number;
|
||||
/** Content-Type of the streamed file */
|
||||
contentType: string;
|
||||
/** Charset of the body part */
|
||||
charset?: string;
|
||||
/** Content-Disposition of the streamed file */
|
||||
disposition?: string;
|
||||
/** Filename of the streamed body part */
|
||||
filename?: string;
|
||||
/** Transfer encoding */
|
||||
encoding?: string;
|
||||
/** If content uses flowed formatting */
|
||||
flowed?: boolean;
|
||||
/** If flowed text uses delSp */
|
||||
delSp?: boolean;
|
||||
};
|
||||
/** Streamed content */
|
||||
content: Readable;
|
||||
}
|
||||
|
||||
export interface AppendResponseObject {
|
||||
/** Full mailbox path where the message was uploaded to */
|
||||
destination: string;
|
||||
/** Mailbox UIDVALIDITY if server has UIDPLUS extension enabled */
|
||||
uidValidity?: bigint;
|
||||
/** UID of the uploaded message if server has UIDPLUS extension enabled */
|
||||
uid?: number;
|
||||
/** Sequence number of the uploaded message if path is currently selected mailbox */
|
||||
seq?: number;
|
||||
}
|
||||
|
||||
export interface CopyResponseObject {
|
||||
/** Path of source mailbox */
|
||||
path: string;
|
||||
/** Path of destination mailbox */
|
||||
destination: string;
|
||||
/** Destination mailbox UIDVALIDITY if server has UIDPLUS extension enabled */
|
||||
uidValidity?: bigint;
|
||||
/** Map of UID values where key is UID in source mailbox and value is the UID for the same message in destination mailbox */
|
||||
uidMap?: Map<number, number>;
|
||||
}
|
||||
|
||||
export interface FetchOptions {
|
||||
/** If true then uses UID numbers instead of sequence numbers */
|
||||
uid?: boolean;
|
||||
/** If set then only messages with a higher modseq value are returned */
|
||||
changedSince?: bigint;
|
||||
/** If true then requests a binary response if the server supports this */
|
||||
binary?: boolean;
|
||||
}
|
||||
|
||||
export interface StoreOptions {
|
||||
/** If true then uses UID numbers instead of sequence numbers */
|
||||
uid?: boolean;
|
||||
/** If set then only messages with a lower or equal modseq value are updated */
|
||||
unchangedSince?: bigint;
|
||||
/** If true then update Gmail labels instead of message flags */
|
||||
useLabels?: boolean;
|
||||
/** If true then does not emit 'flags' event */
|
||||
silent?: boolean;
|
||||
}
|
||||
|
||||
export interface MailboxOpenOptions {
|
||||
/** If true then opens mailbox in read-only mode */
|
||||
readOnly?: boolean;
|
||||
/** Optional description for mailbox lock tracking */
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface ExpungeEvent {
|
||||
/** Mailbox path */
|
||||
path: string;
|
||||
/** Sequence number (if vanished is false) */
|
||||
seq?: number;
|
||||
/** UID number (if vanished is true or QRESYNC is enabled) */
|
||||
uid?: number;
|
||||
/** True if message was expunged using VANISHED response */
|
||||
vanished: boolean;
|
||||
/** True if VANISHED EARLIER response */
|
||||
earlier?: boolean;
|
||||
}
|
||||
|
||||
export interface ExistsEvent {
|
||||
/** Mailbox path */
|
||||
path: string;
|
||||
/** Updated count of messages */
|
||||
count: number;
|
||||
/** Message count before this update */
|
||||
prevCount: number;
|
||||
}
|
||||
|
||||
export interface FlagsEvent {
|
||||
/** Mailbox path */
|
||||
path: string;
|
||||
/** Sequence number of updated message */
|
||||
seq: number;
|
||||
/** UID number of updated message (if server provided this value) */
|
||||
uid?: number;
|
||||
/** Updated modseq number for the mailbox */
|
||||
modseq?: bigint;
|
||||
/** A set of all flags for the updated message */
|
||||
flags: Set<string>;
|
||||
/** Flag color if message is flagged */
|
||||
flagColor?: string;
|
||||
}
|
||||
|
||||
export interface LogEvent {
|
||||
/** Log level */
|
||||
level: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
|
||||
/** Timestamp */
|
||||
t: number;
|
||||
/** Connection ID */
|
||||
cid: string;
|
||||
/** Log order number */
|
||||
lo: number;
|
||||
/** Additional log data */
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface ResponseEvent {
|
||||
/** Response type */
|
||||
response: string;
|
||||
/** Response code */
|
||||
code?: string;
|
||||
}
|
||||
|
||||
export class AuthenticationFailure extends Error {
|
||||
authenticationFailed: true;
|
||||
serverResponseCode?: string;
|
||||
response?: string;
|
||||
oauthError?: any;
|
||||
}
|
||||
|
||||
export class ImapFlow extends EventEmitter {
|
||||
/** Current module version */
|
||||
static version: string;
|
||||
|
||||
/** Instance ID for logs */
|
||||
id: string;
|
||||
|
||||
/** Server identification info */
|
||||
serverInfo: IdInfoObject | null;
|
||||
|
||||
/** Is the connection currently encrypted or not */
|
||||
secureConnection: boolean;
|
||||
|
||||
/** Active IMAP capabilities */
|
||||
capabilities: Map<string, boolean | number>;
|
||||
|
||||
/** Enabled capabilities */
|
||||
enabled: Set<string>;
|
||||
|
||||
/** Is the connection currently usable or not */
|
||||
usable: boolean;
|
||||
|
||||
/** Currently authenticated user */
|
||||
authenticated: string | boolean;
|
||||
|
||||
/** Currently selected mailbox */
|
||||
mailbox: MailboxObject | false;
|
||||
|
||||
/** Is current mailbox idling */
|
||||
idling: boolean;
|
||||
|
||||
constructor(options: ImapFlowOptions);
|
||||
|
||||
/** Initiates a connection against IMAP server */
|
||||
connect(): Promise<void>;
|
||||
|
||||
/** Graceful connection close by sending logout command to server */
|
||||
logout(): Promise<void>;
|
||||
|
||||
/** Closes TCP connection without notifying the server */
|
||||
close(): void;
|
||||
|
||||
/** Returns current quota */
|
||||
getQuota(path?: string): Promise<QuotaResponse | false>;
|
||||
|
||||
/** Lists available mailboxes as an Array */
|
||||
list(options?: ListOptions): Promise<ListResponse[]>;
|
||||
|
||||
/** Lists available mailboxes as a tree structured object */
|
||||
listTree(options?: ListOptions): Promise<ListTreeResponse>;
|
||||
|
||||
/** Performs a no-op call against server */
|
||||
noop(): Promise<void>;
|
||||
|
||||
/** Creates a new mailbox folder */
|
||||
mailboxCreate(path: string | string[]): Promise<MailboxCreateResponse>;
|
||||
|
||||
/** Renames a mailbox */
|
||||
mailboxRename(path: string | string[], newPath: string | string[]): Promise<MailboxRenameResponse>;
|
||||
|
||||
/** Deletes a mailbox */
|
||||
mailboxDelete(path: string | string[]): Promise<MailboxDeleteResponse>;
|
||||
|
||||
/** Subscribes to a mailbox */
|
||||
mailboxSubscribe(path: string | string[]): Promise<boolean>;
|
||||
|
||||
/** Unsubscribes from a mailbox */
|
||||
mailboxUnsubscribe(path: string | string[]): Promise<boolean>;
|
||||
|
||||
/** Opens a mailbox to access messages */
|
||||
mailboxOpen(path: string | string[], options?: MailboxOpenOptions): Promise<MailboxObject>;
|
||||
|
||||
/** Closes a previously opened mailbox */
|
||||
mailboxClose(): Promise<boolean>;
|
||||
|
||||
/** Requests the status of the indicated mailbox */
|
||||
status(path: string, query: {
|
||||
messages?: boolean;
|
||||
recent?: boolean;
|
||||
uidNext?: boolean;
|
||||
uidValidity?: boolean;
|
||||
unseen?: boolean;
|
||||
highestModseq?: boolean;
|
||||
}): Promise<StatusObject>;
|
||||
|
||||
/** Starts listening for new or deleted messages from the currently opened mailbox */
|
||||
idle(): Promise<boolean>;
|
||||
|
||||
/** Sets flags for a message or message range */
|
||||
messageFlagsSet(range: SequenceString | number[] | SearchObject, flags: string[], options?: StoreOptions): Promise<boolean>;
|
||||
|
||||
/** Adds flags for a message or message range */
|
||||
messageFlagsAdd(range: SequenceString | number[] | SearchObject, flags: string[], options?: StoreOptions): Promise<boolean>;
|
||||
|
||||
/** Remove specific flags from a message or message range */
|
||||
messageFlagsRemove(range: SequenceString | number[] | SearchObject, flags: string[], options?: StoreOptions): Promise<boolean>;
|
||||
|
||||
/** Sets a colored flag for an email */
|
||||
setFlagColor(range: SequenceString | number[] | SearchObject, color: string, options?: StoreOptions): Promise<boolean>;
|
||||
|
||||
/** Delete messages from the currently opened mailbox */
|
||||
messageDelete(range: SequenceString | number[] | SearchObject, options?: { uid?: boolean }): Promise<boolean>;
|
||||
|
||||
/** Appends a new message to a mailbox */
|
||||
append(path: string, content: string | Buffer, flags?: string[], idate?: Date | string): Promise<AppendResponseObject | false>;
|
||||
|
||||
/** Copies messages from current mailbox to destination mailbox */
|
||||
messageCopy(range: SequenceString | number[] | SearchObject, destination: string, options?: { uid?: boolean }): Promise<CopyResponseObject | false>;
|
||||
|
||||
/** Moves messages from current mailbox to destination mailbox */
|
||||
messageMove(range: SequenceString | number[] | SearchObject, destination: string, options?: { uid?: boolean }): Promise<CopyResponseObject | false>;
|
||||
|
||||
/** Search messages from the currently opened mailbox */
|
||||
search(query: SearchObject, options?: { uid?: boolean }): Promise<number[] | false>;
|
||||
|
||||
/** Fetch messages from the currently opened mailbox */
|
||||
fetch(range: SequenceString | number[] | SearchObject, query: FetchQueryObject, options?: FetchOptions): AsyncIterableIterator<FetchMessageObject>;
|
||||
|
||||
/** Fetch all messages from the currently opened mailbox */
|
||||
fetchAll(range: SequenceString | number[] | SearchObject, query: FetchQueryObject, options?: FetchOptions): Promise<FetchMessageObject[]>;
|
||||
|
||||
/** Fetch a single message from the currently opened mailbox */
|
||||
fetchOne(seq: SequenceString, query: FetchQueryObject, options?: FetchOptions): Promise<FetchMessageObject | false>;
|
||||
|
||||
/** Download either full rfc822 formatted message or a specific bodystructure part as a Stream */
|
||||
download(range: SequenceString, part?: string, options?: {
|
||||
uid?: boolean;
|
||||
maxBytes?: number;
|
||||
chunkSize?: number;
|
||||
}): Promise<DownloadObject>;
|
||||
|
||||
/** Fetch multiple attachments as Buffer values */
|
||||
downloadMany(range: SequenceString, parts: string[], options?: { uid?: boolean }): Promise<{
|
||||
[part: string]: {
|
||||
meta: {
|
||||
contentType?: string;
|
||||
charset?: string;
|
||||
disposition?: string;
|
||||
filename?: string;
|
||||
encoding?: string;
|
||||
};
|
||||
content: Buffer | null;
|
||||
}
|
||||
}>;
|
||||
|
||||
/** Opens a mailbox if not already open and returns a lock */
|
||||
getMailboxLock(path: string | string[], options?: MailboxOpenOptions): Promise<MailboxLockObject>;
|
||||
|
||||
/** Connection close event */
|
||||
on(event: 'close', listener: () => void): this;
|
||||
|
||||
/** Error event */
|
||||
on(event: 'error', listener: (error: Error) => void): this;
|
||||
|
||||
/** Message count in currently opened mailbox changed */
|
||||
on(event: 'exists', listener: (data: ExistsEvent) => void): this;
|
||||
|
||||
/** Deleted message sequence number in currently opened mailbox */
|
||||
on(event: 'expunge', listener: (data: ExpungeEvent) => void): this;
|
||||
|
||||
/** Flags were updated for a message */
|
||||
on(event: 'flags', listener: (data: FlagsEvent) => void): this;
|
||||
|
||||
/** Mailbox was opened */
|
||||
on(event: 'mailboxOpen', listener: (mailbox: MailboxObject) => void): this;
|
||||
|
||||
/** Mailbox was closed */
|
||||
on(event: 'mailboxClose', listener: (mailbox: MailboxObject) => void): this;
|
||||
|
||||
/** Log event if emitLogs=true */
|
||||
on(event: 'log', listener: (entry: LogEvent) => void): this;
|
||||
|
||||
/** Response event */
|
||||
on(event: 'response', listener: (response: ResponseEvent) => void): this;
|
||||
}
|
||||
3678
backend/node_modules/imapflow/lib/imap-flow.js
generated
vendored
Normal file
3678
backend/node_modules/imapflow/lib/imap-flow.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
46
backend/node_modules/imapflow/lib/jp-decoder.js
generated
vendored
Normal file
46
backend/node_modules/imapflow/lib/jp-decoder.js
generated
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
'use strict';
|
||||
|
||||
const { Transform } = require('stream');
|
||||
const encodingJapanese = require('encoding-japanese');
|
||||
|
||||
class JPDecoder extends Transform {
|
||||
constructor(charset) {
|
||||
super();
|
||||
|
||||
this.charset = charset;
|
||||
this.chunks = [];
|
||||
this.chunklen = 0;
|
||||
}
|
||||
|
||||
_transform(chunk, encoding, done) {
|
||||
if (typeof chunk === 'string') {
|
||||
chunk = Buffer.from(chunk, encoding);
|
||||
}
|
||||
|
||||
this.chunks.push(chunk);
|
||||
this.chunklen += chunk.length;
|
||||
done();
|
||||
}
|
||||
|
||||
_flush(done) {
|
||||
let input = Buffer.concat(this.chunks, this.chunklen);
|
||||
try {
|
||||
let output = encodingJapanese.convert(input, {
|
||||
to: 'UNICODE', // to_encoding
|
||||
from: this.charset, // from_encoding
|
||||
type: 'string'
|
||||
});
|
||||
if (typeof output === 'string') {
|
||||
output = Buffer.from(output);
|
||||
}
|
||||
this.push(output);
|
||||
} catch {
|
||||
// keep as is on errors
|
||||
this.push(input);
|
||||
}
|
||||
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.JPDecoder = JPDecoder;
|
||||
37
backend/node_modules/imapflow/lib/limited-passthrough.js
generated
vendored
Normal file
37
backend/node_modules/imapflow/lib/limited-passthrough.js
generated
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
'use strict';
|
||||
|
||||
const { Transform } = require('stream');
|
||||
|
||||
class LimitedPassthrough extends Transform {
|
||||
constructor(options) {
|
||||
super();
|
||||
this.options = options || {};
|
||||
this.maxBytes = this.options.maxBytes || Infinity;
|
||||
this.processed = 0;
|
||||
this.limited = false;
|
||||
}
|
||||
|
||||
_transform(chunk, encoding, done) {
|
||||
if (this.limited) {
|
||||
return done();
|
||||
}
|
||||
|
||||
if (this.processed + chunk.length > this.maxBytes) {
|
||||
if (this.maxBytes - this.processed < 1) {
|
||||
return done();
|
||||
}
|
||||
|
||||
chunk = chunk.slice(0, this.maxBytes - this.processed);
|
||||
}
|
||||
|
||||
this.processed += chunk.length;
|
||||
if (this.processed >= this.maxBytes) {
|
||||
this.limited = true;
|
||||
}
|
||||
|
||||
this.push(chunk);
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.LimitedPassthrough = LimitedPassthrough;
|
||||
5
backend/node_modules/imapflow/lib/logger.js
generated
vendored
Normal file
5
backend/node_modules/imapflow/lib/logger.js
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
const logger = require('pino')();
|
||||
logger.level = 'trace';
|
||||
module.exports = logger;
|
||||
120
backend/node_modules/imapflow/lib/proxy-connection.js
generated
vendored
Normal file
120
backend/node_modules/imapflow/lib/proxy-connection.js
generated
vendored
Normal file
@@ -0,0 +1,120 @@
|
||||
'use strict';
|
||||
|
||||
const httpProxyClient = require('nodemailer/lib/smtp-connection/http-proxy-client');
|
||||
const { SocksClient } = require('socks');
|
||||
const util = require('util');
|
||||
const httpProxyClientAsync = util.promisify(httpProxyClient);
|
||||
const dns = require('dns').promises;
|
||||
const net = require('net');
|
||||
|
||||
const proxyConnection = async (logger, connectionUrl, host, port) => {
|
||||
let proxyUrl = new URL(connectionUrl);
|
||||
|
||||
let protocol = proxyUrl.protocol.replace(/:$/, '').toLowerCase();
|
||||
|
||||
if (!net.isIP(host)) {
|
||||
let resolveResult = await dns.resolve(host);
|
||||
if (resolveResult && resolveResult.length) {
|
||||
host = resolveResult[0];
|
||||
}
|
||||
}
|
||||
|
||||
switch (protocol) {
|
||||
// Connect using a HTTP CONNECT method
|
||||
case 'http':
|
||||
case 'https': {
|
||||
try {
|
||||
let socket = await httpProxyClientAsync(proxyUrl.href, port, host);
|
||||
if (socket) {
|
||||
if (proxyUrl.password) {
|
||||
proxyUrl.password = '(hidden)';
|
||||
}
|
||||
logger.info({
|
||||
msg: 'Established a socket via HTTP proxy',
|
||||
proxyUrl: proxyUrl.href,
|
||||
port,
|
||||
host
|
||||
});
|
||||
}
|
||||
return socket;
|
||||
} catch (err) {
|
||||
if (proxyUrl.password) {
|
||||
proxyUrl.password = '(hidden)';
|
||||
}
|
||||
logger.error({
|
||||
msg: 'Failed to establish a socket via HTTP proxy',
|
||||
proxyUrl: proxyUrl.href,
|
||||
port,
|
||||
host,
|
||||
err
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// SOCKS proxy
|
||||
case 'socks':
|
||||
case 'socks5':
|
||||
case 'socks4':
|
||||
case 'socks4a': {
|
||||
let proxyType = Number(protocol.replace(/\D/g, '')) || 5;
|
||||
|
||||
let targetHost = proxyUrl.hostname;
|
||||
if (!net.isIP(targetHost)) {
|
||||
let resolveResult = await dns.resolve(targetHost);
|
||||
if (resolveResult && resolveResult.length) {
|
||||
targetHost = resolveResult[0];
|
||||
}
|
||||
}
|
||||
|
||||
let connectionOpts = {
|
||||
proxy: {
|
||||
host: targetHost,
|
||||
port: Number(proxyUrl.port) || 1080,
|
||||
type: proxyType
|
||||
},
|
||||
destination: {
|
||||
host,
|
||||
port
|
||||
},
|
||||
command: 'connect',
|
||||
set_tcp_nodelay: true
|
||||
};
|
||||
|
||||
if (proxyUrl.username || proxyUrl.password) {
|
||||
connectionOpts.proxy.userId = proxyUrl.username;
|
||||
connectionOpts.proxy.password = proxyUrl.password;
|
||||
}
|
||||
|
||||
try {
|
||||
const info = await SocksClient.createConnection(connectionOpts);
|
||||
if (info && info.socket) {
|
||||
if (proxyUrl.password) {
|
||||
proxyUrl.password = '(hidden)';
|
||||
}
|
||||
logger.info({
|
||||
msg: 'Established a socket via SOCKS proxy',
|
||||
proxyUrl: proxyUrl.href,
|
||||
port,
|
||||
host
|
||||
});
|
||||
}
|
||||
return info.socket;
|
||||
} catch (err) {
|
||||
if (proxyUrl.password) {
|
||||
proxyUrl.password = '(hidden)';
|
||||
}
|
||||
logger.error({
|
||||
msg: 'Failed to establish a socket via SOCKS proxy',
|
||||
proxyUrl: proxyUrl.href,
|
||||
port,
|
||||
host,
|
||||
err
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { proxyConnection };
|
||||
435
backend/node_modules/imapflow/lib/search-compiler.js
generated
vendored
Normal file
435
backend/node_modules/imapflow/lib/search-compiler.js
generated
vendored
Normal file
@@ -0,0 +1,435 @@
|
||||
/* eslint no-control-regex:0 */
|
||||
|
||||
'use strict';
|
||||
|
||||
const { formatDate, formatFlag, canUseFlag, isDate } = require('./tools.js');
|
||||
|
||||
/**
|
||||
* Sets a boolean flag in the IMAP search attributes.
|
||||
* Automatically handles UN- prefixing for falsy values.
|
||||
*
|
||||
* @param {Array} attributes - Array to append the attribute to
|
||||
* @param {string} term - The flag name (e.g., 'SEEN', 'DELETED')
|
||||
* @param {boolean} value - Whether to set or unset the flag
|
||||
* @example
|
||||
* setBoolOpt(attributes, 'SEEN', false) // Adds 'UNSEEN'
|
||||
* setBoolOpt(attributes, 'UNSEEN', false) // Adds 'SEEN' (removes UN prefix)
|
||||
*/
|
||||
let setBoolOpt = (attributes, term, value) => {
|
||||
if (!value) {
|
||||
// For falsy values, toggle the UN- prefix
|
||||
if (/^un/i.test(term)) {
|
||||
// Remove existing UN prefix
|
||||
term = term.slice(2);
|
||||
} else {
|
||||
// Add UN prefix
|
||||
term = 'UN' + term;
|
||||
}
|
||||
}
|
||||
|
||||
attributes.push({ type: 'ATOM', value: term.toUpperCase() });
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a search option with its value(s) to the attributes array.
|
||||
* Handles NOT operations and array values.
|
||||
*
|
||||
* @param {Array} attributes - Array to append the attribute to
|
||||
* @param {string} term - The search term (e.g., 'FROM', 'SUBJECT')
|
||||
* @param {*} value - The value for the search term (string, array, or falsy for NOT)
|
||||
* @param {string} [type='ATOM'] - The attribute type
|
||||
*/
|
||||
let setOpt = (attributes, term, value, type) => {
|
||||
type = type || 'ATOM';
|
||||
|
||||
// Handle NOT operations for false or null values
|
||||
if (value === false || value === null) {
|
||||
attributes.push({ type, value: 'NOT' });
|
||||
}
|
||||
|
||||
attributes.push({ type, value: term.toUpperCase() });
|
||||
|
||||
// Handle array values (e.g., multiple UIDs)
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(entry => attributes.push({ type, value: (entry || '').toString() }));
|
||||
} else {
|
||||
attributes.push({ type, value: value.toString() });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Processes date fields for IMAP search.
|
||||
* Converts JavaScript dates to IMAP date format.
|
||||
*
|
||||
* @param {Array} attributes - Array to append the attribute to
|
||||
* @param {string} term - The date search term (e.g., 'BEFORE', 'SINCE')
|
||||
* @param {*} value - Date value to format
|
||||
*/
|
||||
let processDateField = (attributes, term, value) => {
|
||||
if (['BEFORE', 'SENTBEFORE'].includes(term.toUpperCase()) && isDate(value) && value.toISOString().substring(11) !== '00:00:00.000Z') {
|
||||
// Set to next day to include current day as well, othwerise BEFORE+AFTER
|
||||
// searches for the same day but different time values do not match anything
|
||||
value = new Date(value.getTime() + 24 * 3600 * 1000);
|
||||
}
|
||||
|
||||
let date = formatDate(value);
|
||||
if (!date) {
|
||||
return;
|
||||
}
|
||||
|
||||
setOpt(attributes, term, date);
|
||||
};
|
||||
|
||||
// Pre-compiled regex for better performance
|
||||
const UNICODE_PATTERN = /[^\x00-\x7F]/;
|
||||
|
||||
/**
|
||||
* Checks if a string contains Unicode characters.
|
||||
* Used to determine if CHARSET UTF-8 needs to be specified.
|
||||
*
|
||||
* @param {*} str - String to check
|
||||
* @returns {boolean} True if string contains non-ASCII characters
|
||||
*/
|
||||
let isUnicodeString = str => {
|
||||
if (!str || typeof str !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Regex test is ~3-5x faster than Buffer.byteLength
|
||||
// Matches any character outside ASCII range (0x00-0x7F)
|
||||
return UNICODE_PATTERN.test(str);
|
||||
};
|
||||
|
||||
/**
|
||||
* Compiles a JavaScript object query into IMAP search command attributes.
|
||||
* Supports standard IMAP search criteria and extensions like OBJECTID and Gmail extensions.
|
||||
*
|
||||
* @param {Object} connection - IMAP connection object
|
||||
* @param {Object} connection.capabilities - Set of server capabilities
|
||||
* @param {Object} connection.enabled - Set of enabled extensions
|
||||
* @param {Object} connection.mailbox - Current mailbox information
|
||||
* @param {Set} connection.mailbox.flags - Available flags in the mailbox
|
||||
* @param {Object} query - Search query object
|
||||
* @returns {Array} Array of IMAP search attributes
|
||||
* @throws {Error} When required server extensions are not available
|
||||
*
|
||||
* @example
|
||||
* // Simple search for unseen messages from a sender
|
||||
* searchCompiler(connection, {
|
||||
* unseen: true,
|
||||
* from: 'sender@example.com'
|
||||
* });
|
||||
*
|
||||
* @example
|
||||
* // Complex OR search with date range
|
||||
* searchCompiler(connection, {
|
||||
* or: [
|
||||
* { from: 'alice@example.com' },
|
||||
* { from: 'bob@example.com' }
|
||||
* ],
|
||||
* since: new Date('2024-01-01')
|
||||
* });
|
||||
*/
|
||||
module.exports.searchCompiler = (connection, query) => {
|
||||
const attributes = [];
|
||||
|
||||
// Track if we need to specify UTF-8 charset
|
||||
let hasUnicode = false;
|
||||
const mailbox = connection.mailbox;
|
||||
|
||||
/**
|
||||
* Recursively walks through the query object and builds IMAP attributes.
|
||||
* @param {Object} params - Query parameters to process
|
||||
*/
|
||||
const walk = params => {
|
||||
Object.keys(params || {}).forEach(term => {
|
||||
switch (term.toUpperCase()) {
|
||||
// Custom sequence range support (non-standard)
|
||||
case 'SEQ':
|
||||
{
|
||||
let value = params[term];
|
||||
if (typeof value === 'number') {
|
||||
value = value.toString();
|
||||
}
|
||||
// Only accept valid sequence strings (no whitespace)
|
||||
if (typeof value === 'string' && /^\S+$/.test(value)) {
|
||||
attributes.push({ type: 'SEQUENCE', value });
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
// Boolean flags that support UN- prefixing
|
||||
case 'ANSWERED':
|
||||
case 'DELETED':
|
||||
case 'DRAFT':
|
||||
case 'FLAGGED':
|
||||
case 'SEEN':
|
||||
case 'UNANSWERED':
|
||||
case 'UNDELETED':
|
||||
case 'UNDRAFT':
|
||||
case 'UNFLAGGED':
|
||||
case 'UNSEEN':
|
||||
// toggles UN-prefix for falsy values
|
||||
setBoolOpt(attributes, term, !!params[term]);
|
||||
break;
|
||||
|
||||
// Simple boolean flags without UN- support
|
||||
case 'ALL':
|
||||
case 'NEW':
|
||||
case 'OLD':
|
||||
case 'RECENT':
|
||||
if (params[term]) {
|
||||
setBoolOpt(attributes, term, true);
|
||||
}
|
||||
break;
|
||||
|
||||
// Numeric comparisons
|
||||
case 'LARGER':
|
||||
case 'SMALLER':
|
||||
case 'MODSEQ':
|
||||
if (params[term]) {
|
||||
setOpt(attributes, term, params[term]);
|
||||
}
|
||||
break;
|
||||
|
||||
// Text search fields - check for Unicode
|
||||
case 'BCC':
|
||||
case 'BODY':
|
||||
case 'CC':
|
||||
case 'FROM':
|
||||
case 'SUBJECT':
|
||||
case 'TEXT':
|
||||
case 'TO':
|
||||
if (isUnicodeString(params[term])) {
|
||||
hasUnicode = true;
|
||||
}
|
||||
if (params[term]) {
|
||||
setOpt(attributes, term, params[term]);
|
||||
}
|
||||
break;
|
||||
|
||||
// UID sequences
|
||||
case 'UID':
|
||||
if (params[term]) {
|
||||
setOpt(attributes, term, params[term], 'SEQUENCE');
|
||||
}
|
||||
break;
|
||||
|
||||
// Email ID support (OBJECTID or Gmail extension)
|
||||
case 'EMAILID':
|
||||
if (connection.capabilities.has('OBJECTID')) {
|
||||
setOpt(attributes, 'EMAILID', params[term]);
|
||||
} else if (connection.capabilities.has('X-GM-EXT-1')) {
|
||||
// Fallback to Gmail message ID
|
||||
setOpt(attributes, 'X-GM-MSGID', params[term]);
|
||||
}
|
||||
break;
|
||||
|
||||
// Thread ID support (OBJECTID or Gmail extension)
|
||||
case 'THREADID':
|
||||
if (connection.capabilities.has('OBJECTID')) {
|
||||
setOpt(attributes, 'THREADID', params[term]);
|
||||
} else if (connection.capabilities.has('X-GM-EXT-1')) {
|
||||
// Fallback to Gmail thread ID
|
||||
setOpt(attributes, 'X-GM-THRID', params[term]);
|
||||
}
|
||||
break;
|
||||
|
||||
// Gmail raw search
|
||||
case 'GMRAW':
|
||||
case 'GMAILRAW': // alias for GMRAW
|
||||
if (connection.capabilities.has('X-GM-EXT-1')) {
|
||||
if (isUnicodeString(params[term])) {
|
||||
hasUnicode = true;
|
||||
}
|
||||
setOpt(attributes, 'X-GM-RAW', params[term]);
|
||||
} else {
|
||||
let error = new Error('Server does not support X-GM-EXT-1 extension required for X-GM-RAW');
|
||||
error.code = 'MissingServerExtension';
|
||||
throw error;
|
||||
}
|
||||
break;
|
||||
|
||||
// Date searches with WITHIN extension support
|
||||
case 'BEFORE':
|
||||
case 'SINCE':
|
||||
{
|
||||
// Use WITHIN extension for better timezone handling if available
|
||||
if (connection.capabilities.has('WITHIN') && isDate(params[term])) {
|
||||
// Convert to seconds ago from now
|
||||
const now = Date.now();
|
||||
const withinSeconds = Math.round(Math.max(0, now - params[term].getTime()) / 1000);
|
||||
let withinKeyword;
|
||||
switch (term.toUpperCase()) {
|
||||
case 'BEFORE':
|
||||
withinKeyword = 'OLDER';
|
||||
break;
|
||||
case 'SINCE':
|
||||
withinKeyword = 'YOUNGER';
|
||||
break;
|
||||
}
|
||||
setOpt(attributes, withinKeyword, withinSeconds.toString());
|
||||
break;
|
||||
}
|
||||
|
||||
// Fallback to standard date search
|
||||
processDateField(attributes, term, params[term]);
|
||||
}
|
||||
break;
|
||||
|
||||
// Standard date searches
|
||||
case 'ON':
|
||||
case 'SENTBEFORE':
|
||||
case 'SENTON':
|
||||
case 'SENTSINCE':
|
||||
processDateField(attributes, term, params[term]);
|
||||
break;
|
||||
|
||||
// Keyword/flag searches
|
||||
case 'KEYWORD':
|
||||
case 'UNKEYWORD':
|
||||
{
|
||||
let flag = formatFlag(params[term]);
|
||||
// Only add if flag is supported or already exists in mailbox
|
||||
if (canUseFlag(mailbox, flag) || mailbox.flags.has(flag)) {
|
||||
setOpt(attributes, term, flag);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
// Header field searches
|
||||
case 'HEADER':
|
||||
if (params[term] && typeof params[term] === 'object') {
|
||||
Object.keys(params[term]).forEach(header => {
|
||||
let value = params[term][header];
|
||||
|
||||
// Allow boolean true to search for header existence
|
||||
if (value === true) {
|
||||
value = '';
|
||||
}
|
||||
|
||||
// Skip non-string values (after true->'' conversion)
|
||||
if (typeof value !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isUnicodeString(value)) {
|
||||
hasUnicode = true;
|
||||
}
|
||||
|
||||
setOpt(attributes, term, [header.toUpperCase().trim(), value]);
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
// NOT operator
|
||||
case 'NOT':
|
||||
{
|
||||
if (!params[term]) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (typeof params[term] === 'object') {
|
||||
attributes.push({ type: 'ATOM', value: 'NOT' });
|
||||
// Recursively process NOT conditions
|
||||
walk(params[term]);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
// OR operator - complex logic for building OR trees
|
||||
case 'OR':
|
||||
{
|
||||
if (!params[term] || !Array.isArray(params[term]) || !params[term].length) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Single element - just process it directly
|
||||
if (params[term].length === 1) {
|
||||
if (typeof params[term][0] === 'object' && params[term][0]) {
|
||||
walk(params[term][0]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a binary tree structure for OR operations.
|
||||
* IMAP OR takes exactly 2 operands, so we need to nest them.
|
||||
*
|
||||
* @param {Array} list - List of conditions to OR together
|
||||
* @returns {Array} Binary tree structure
|
||||
*/
|
||||
let genOrTree = list => {
|
||||
let group = false;
|
||||
let groups = [];
|
||||
|
||||
// Group items in pairs
|
||||
list.forEach((entry, i) => {
|
||||
if (i % 2 === 0) {
|
||||
group = [entry];
|
||||
} else {
|
||||
group.push(entry);
|
||||
groups.push(group);
|
||||
group = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Handle odd number of items
|
||||
if (group && group.length) {
|
||||
while (group.length === 1 && Array.isArray(group[0])) {
|
||||
group = group[0];
|
||||
}
|
||||
|
||||
groups.push(group);
|
||||
}
|
||||
|
||||
// Recursively group until we have a binary tree
|
||||
while (groups.length > 2) {
|
||||
groups = genOrTree(groups);
|
||||
}
|
||||
|
||||
// Flatten single-element arrays
|
||||
while (groups.length === 1 && Array.isArray(groups[0])) {
|
||||
groups = groups[0];
|
||||
}
|
||||
|
||||
return groups;
|
||||
};
|
||||
|
||||
/**
|
||||
* Walks the OR tree and generates IMAP commands.
|
||||
* @param {Array|Object} entry - Tree node to process
|
||||
*/
|
||||
let walkOrTree = entry => {
|
||||
if (Array.isArray(entry)) {
|
||||
// Only add OR for multiple items
|
||||
if (entry.length > 1) {
|
||||
attributes.push({ type: 'ATOM', value: 'OR' });
|
||||
}
|
||||
entry.forEach(walkOrTree);
|
||||
return;
|
||||
}
|
||||
if (entry && typeof entry === 'object') {
|
||||
walk(entry);
|
||||
}
|
||||
};
|
||||
|
||||
walkOrTree(genOrTree(params[term]));
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Process the query
|
||||
walk(query);
|
||||
|
||||
// If we encountered Unicode strings and UTF-8 is not already accepted,
|
||||
// prepend CHARSET UTF-8 to the search command
|
||||
if (hasUnicode && !connection.enabled.has('UTF8=ACCEPT')) {
|
||||
attributes.unshift({ type: 'ATOM', value: 'UTF-8' });
|
||||
attributes.unshift({ type: 'ATOM', value: 'CHARSET' });
|
||||
}
|
||||
|
||||
return attributes;
|
||||
};
|
||||
307
backend/node_modules/imapflow/lib/special-use.js
generated
vendored
Normal file
307
backend/node_modules/imapflow/lib/special-use.js
generated
vendored
Normal file
@@ -0,0 +1,307 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
flags: ['\\All', '\\Archive', '\\Drafts', '\\Flagged', '\\Junk', '\\Sent', '\\Trash'],
|
||||
names: {
|
||||
'\\Sent': [
|
||||
'aika',
|
||||
'bidaliak',
|
||||
'bidalita',
|
||||
'dihantar',
|
||||
'e rometsweng',
|
||||
'e tindami',
|
||||
'elküldött',
|
||||
'elküldöttek',
|
||||
'elementos enviados',
|
||||
'éléments envoyés',
|
||||
'enviadas',
|
||||
'enviadas',
|
||||
'enviados',
|
||||
'enviats',
|
||||
'envoyés',
|
||||
'ethunyelweyo',
|
||||
'expediate',
|
||||
'ezipuru',
|
||||
'gesendete',
|
||||
'gesendete elemente',
|
||||
'gestuur',
|
||||
'gönderilmiş öğeler',
|
||||
'göndərilənlər',
|
||||
'iberilen',
|
||||
'inviati',
|
||||
'išsiųstieji',
|
||||
'kuthunyelwe',
|
||||
'lasa',
|
||||
'lähetetyt',
|
||||
'messages envoyés',
|
||||
'naipadala',
|
||||
'nalefa',
|
||||
'napadala',
|
||||
'nosūtītās ziņas',
|
||||
'odeslané',
|
||||
'odeslaná pošta',
|
||||
'padala',
|
||||
'poslane',
|
||||
'poslano',
|
||||
'poslano',
|
||||
'poslané',
|
||||
'poslato',
|
||||
'saadetud',
|
||||
'saadetud kirjad',
|
||||
'saadetud üksused',
|
||||
'sendt',
|
||||
'sendt',
|
||||
'sent',
|
||||
'sent items',
|
||||
'sent messages',
|
||||
'sända poster',
|
||||
'sänt',
|
||||
'terkirim',
|
||||
'ti fi ranṣẹ',
|
||||
'të dërguara',
|
||||
'verzonden',
|
||||
'vilivyotumwa',
|
||||
'wysłane',
|
||||
'đã gửi',
|
||||
'σταλθέντα',
|
||||
'жиберилген',
|
||||
'жіберілгендер',
|
||||
'изпратени',
|
||||
'илгээсэн',
|
||||
'ирсол шуд',
|
||||
'испратено',
|
||||
'надіслані',
|
||||
'отправленные',
|
||||
'пасланыя',
|
||||
'юборилган',
|
||||
'ուղարկված',
|
||||
'נשלחו',
|
||||
'פריטים שנשלחו',
|
||||
'المرسلة',
|
||||
'بھیجے گئے',
|
||||
'سوزمژہ',
|
||||
'لېګل شوی',
|
||||
'موارد ارسال شده',
|
||||
'पाठविले',
|
||||
'पाठविलेले',
|
||||
'प्रेषित',
|
||||
'भेजा गया',
|
||||
'প্রেরিত',
|
||||
'প্রেরিত',
|
||||
'প্ৰেৰিত',
|
||||
'ਭੇਜੇ',
|
||||
'મોકલેલા',
|
||||
'ପଠାଗଲା',
|
||||
'அனுப்பியவை',
|
||||
'పంపించబడింది',
|
||||
'ಕಳುಹಿಸಲಾದ',
|
||||
'അയച്ചു',
|
||||
'යැවු පණිවුඩ',
|
||||
'ส่งแล้ว',
|
||||
'გაგზავნილი',
|
||||
'የተላኩ',
|
||||
'បានផ្ញើ',
|
||||
'寄件備份',
|
||||
'寄件備份',
|
||||
'已发信息',
|
||||
'送信済みメール',
|
||||
'발신 메시지',
|
||||
'보낸 편지함'
|
||||
],
|
||||
'\\Trash': [
|
||||
'articole șterse',
|
||||
'bin',
|
||||
'borttagna objekt',
|
||||
'deleted',
|
||||
'deleted items',
|
||||
'deleted messages',
|
||||
'elementi eliminati',
|
||||
'elementos borrados',
|
||||
'elementos eliminados',
|
||||
'gelöschte objekte',
|
||||
'gelöschte elemente',
|
||||
'item dipadam',
|
||||
'itens apagados',
|
||||
'itens excluídos',
|
||||
'kustutatud üksused',
|
||||
'mục đã xóa',
|
||||
'odstraněné položky',
|
||||
'odstraněná pošta',
|
||||
'pesan terhapus',
|
||||
'poistetut',
|
||||
'praht',
|
||||
'prügikast',
|
||||
'silinmiş öğeler',
|
||||
'slettede beskeder',
|
||||
'slettede elementer',
|
||||
'trash',
|
||||
'törölt elemek',
|
||||
'törölt',
|
||||
'usunięte wiadomości',
|
||||
'verwijderde items',
|
||||
'vymazané správy',
|
||||
'éléments supprimés',
|
||||
'видалені',
|
||||
'жойылғандар',
|
||||
'удаленные',
|
||||
'פריטים שנמחקו',
|
||||
'العناصر المحذوفة',
|
||||
'موارد حذف شده',
|
||||
'รายการที่ลบ',
|
||||
'已删除邮件',
|
||||
'已刪除項目',
|
||||
'已刪除項目'
|
||||
],
|
||||
'\\Junk': [
|
||||
'bulk mail',
|
||||
'correo no deseado',
|
||||
'courrier indésirable',
|
||||
'istenmeyen',
|
||||
'istenmeyen e-posta',
|
||||
'junk',
|
||||
'junk e-mail',
|
||||
'junk email',
|
||||
'junk-e-mail',
|
||||
'levélszemét',
|
||||
'nevyžiadaná pošta',
|
||||
'nevyžádaná pošta',
|
||||
'no deseado',
|
||||
'posta indesiderata',
|
||||
'pourriel',
|
||||
'roskaposti',
|
||||
'rämpspost',
|
||||
'skräppost',
|
||||
'spam',
|
||||
'spam',
|
||||
'spamowanie',
|
||||
'søppelpost',
|
||||
'thư rác',
|
||||
'wiadomości-śmieci',
|
||||
'спам',
|
||||
'דואר זבל',
|
||||
'الرسائل العشوائية',
|
||||
'هرزنامه',
|
||||
'สแปม',
|
||||
'垃圾郵件',
|
||||
'垃圾邮件',
|
||||
'垃圾電郵'
|
||||
],
|
||||
'\\Drafts': [
|
||||
'ba brouillon',
|
||||
'borrador',
|
||||
'borrador',
|
||||
'borradores',
|
||||
'bozze',
|
||||
'brouillons',
|
||||
'bản thảo',
|
||||
'ciorne',
|
||||
'concepten',
|
||||
'draf',
|
||||
'draft',
|
||||
'drafts',
|
||||
'drög',
|
||||
'entwürfe',
|
||||
'esborranys',
|
||||
'garalamalar',
|
||||
'ihe edeturu',
|
||||
'iidrafti',
|
||||
'izinhlaka',
|
||||
'juodraščiai',
|
||||
'kladd',
|
||||
'kladder',
|
||||
'koncepty',
|
||||
'koncepty',
|
||||
'konsep',
|
||||
'konsepte',
|
||||
'kopie robocze',
|
||||
'layihələr',
|
||||
'luonnokset',
|
||||
'melnraksti',
|
||||
'meralo',
|
||||
'mesazhe të padërguara',
|
||||
'mga draft',
|
||||
'mustandid',
|
||||
'nacrti',
|
||||
'nacrti',
|
||||
'osnutki',
|
||||
'piszkozatok',
|
||||
'rascunhos',
|
||||
'rasimu',
|
||||
'skice',
|
||||
'taslaklar',
|
||||
'tsararrun saƙonni',
|
||||
'utkast',
|
||||
'vakiraoka',
|
||||
'vázlatok',
|
||||
'zirriborroak',
|
||||
'àwọn àkọpamọ́',
|
||||
'πρόχειρα',
|
||||
'жобалар',
|
||||
'нацрти',
|
||||
'нооргууд',
|
||||
'сиёҳнавис',
|
||||
'хомаки хатлар',
|
||||
'чарнавікі',
|
||||
'чернетки',
|
||||
'чернови',
|
||||
'черновики',
|
||||
'черновиктер',
|
||||
'սևագրեր',
|
||||
'טיוטות',
|
||||
'مسودات',
|
||||
'مسودات',
|
||||
'موسودې',
|
||||
'پیش نویسها',
|
||||
'ڈرافٹ/',
|
||||
'ड्राफ़्ट',
|
||||
'प्रारूप',
|
||||
'খসড়া',
|
||||
'খসড়া',
|
||||
'ড্ৰাফ্ট',
|
||||
'ਡ੍ਰਾਫਟ',
|
||||
'ડ્રાફ્ટસ',
|
||||
'ଡ୍ରାଫ୍ଟ',
|
||||
'வரைவுகள்',
|
||||
'చిత్తు ప్రతులు',
|
||||
'ಕರಡುಗಳು',
|
||||
'കരടുകള്',
|
||||
'කෙටුම් පත්',
|
||||
'ฉบับร่าง',
|
||||
'მონახაზები',
|
||||
'ረቂቆች',
|
||||
'សារព្រាង',
|
||||
'下書き',
|
||||
'草稿',
|
||||
'草稿',
|
||||
'草稿',
|
||||
'임시 보관함'
|
||||
],
|
||||
|
||||
'\\Archive': ['archive']
|
||||
},
|
||||
|
||||
specialUse(hasSpecialUseExtension, folder) {
|
||||
let result;
|
||||
|
||||
if (hasSpecialUseExtension) {
|
||||
result = {
|
||||
flag: module.exports.flags.find(flag => folder.flags.has(flag)),
|
||||
source: 'extension'
|
||||
};
|
||||
}
|
||||
|
||||
if (!result || !result.flag) {
|
||||
let name = folder.name
|
||||
.toLowerCase()
|
||||
.replace(/\u200e/g, '')
|
||||
.trim();
|
||||
|
||||
result = {
|
||||
flag: Object.keys(module.exports.names).find(flag => module.exports.names[flag].includes(name)),
|
||||
source: 'name'
|
||||
};
|
||||
}
|
||||
|
||||
return result && result.flag ? result : { flag: null };
|
||||
}
|
||||
};
|
||||
898
backend/node_modules/imapflow/lib/tools.js
generated
vendored
Normal file
898
backend/node_modules/imapflow/lib/tools.js
generated
vendored
Normal file
@@ -0,0 +1,898 @@
|
||||
/* eslint no-control-regex:0 */
|
||||
|
||||
'use strict';
|
||||
|
||||
const libmime = require('libmime');
|
||||
const { resolveCharset } = require('./charsets');
|
||||
const { compiler } = require('./handler/imap-handler');
|
||||
const { createHash } = require('crypto');
|
||||
const { JPDecoder } = require('./jp-decoder');
|
||||
const iconv = require('iconv-lite');
|
||||
|
||||
const FLAG_COLORS = ['red', 'orange', 'yellow', 'green', 'blue', 'purple', 'grey'];
|
||||
|
||||
class AuthenticationFailure extends Error {
|
||||
authenticationFailed = true;
|
||||
}
|
||||
|
||||
const tools = {
|
||||
encodePath(connection, path) {
|
||||
path = (path || '').toString();
|
||||
if (!connection.enabled.has('UTF8=ACCEPT') && /[&\x00-\x08\x0b-\x0c\x0e-\x1f\u0080-\uffff]/.test(path)) {
|
||||
try {
|
||||
path = iconv.encode(path, 'utf-7-imap').toString();
|
||||
} catch {
|
||||
// ignore, keep name as is
|
||||
}
|
||||
}
|
||||
return path;
|
||||
},
|
||||
|
||||
decodePath(connection, path) {
|
||||
path = (path || '').toString();
|
||||
if (!connection.enabled.has('UTF8=ACCEPT') && /[&]/.test(path)) {
|
||||
try {
|
||||
path = iconv.decode(Buffer.from(path), 'utf-7-imap').toString();
|
||||
} catch {
|
||||
// ignore, keep name as is
|
||||
}
|
||||
}
|
||||
return path;
|
||||
},
|
||||
|
||||
normalizePath(connection, path, skipNamespace) {
|
||||
if (Array.isArray(path)) {
|
||||
path = path.join((connection.namespace && connection.namespace.delimiter) || '');
|
||||
}
|
||||
|
||||
if (path.toUpperCase() === 'INBOX') {
|
||||
// inbox is not case sensitive
|
||||
return 'INBOX';
|
||||
}
|
||||
|
||||
// ensure namespace prefix if needed
|
||||
if (!skipNamespace && connection.namespace && connection.namespace.prefix && path.indexOf(connection.namespace.prefix) !== 0) {
|
||||
path = connection.namespace.prefix + path;
|
||||
}
|
||||
|
||||
return path;
|
||||
},
|
||||
|
||||
comparePaths(connection, a, b) {
|
||||
if (!a || !b) {
|
||||
return false;
|
||||
}
|
||||
return tools.normalizePath(connection, a) === tools.normalizePath(connection, b);
|
||||
},
|
||||
|
||||
updateCapabilities(list) {
|
||||
let map = new Map();
|
||||
|
||||
if (list && Array.isArray(list)) {
|
||||
list.forEach(val => {
|
||||
if (typeof val.value !== 'string') {
|
||||
return false;
|
||||
}
|
||||
let capability = val.value.toUpperCase().trim();
|
||||
|
||||
if (capability === 'IMAP4REV1') {
|
||||
map.set('IMAP4rev1', true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (capability.indexOf('APPENDLIMIT=') === 0) {
|
||||
let splitPos = capability.indexOf('=');
|
||||
let appendLimit = Number(capability.substr(splitPos + 1)) || 0;
|
||||
map.set('APPENDLIMIT', appendLimit);
|
||||
return;
|
||||
}
|
||||
|
||||
map.set(capability, true);
|
||||
});
|
||||
}
|
||||
|
||||
return map;
|
||||
},
|
||||
|
||||
AuthenticationFailure,
|
||||
|
||||
getStatusCode(response) {
|
||||
return response &&
|
||||
response.attributes &&
|
||||
response.attributes[0] &&
|
||||
response.attributes[0].section &&
|
||||
response.attributes[0].section[0] &&
|
||||
typeof response.attributes[0].section[0].value === 'string'
|
||||
? response.attributes[0].section[0].value.toUpperCase().trim()
|
||||
: false;
|
||||
},
|
||||
|
||||
async getErrorText(response) {
|
||||
if (!response) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (await compiler(response)).toString();
|
||||
},
|
||||
|
||||
async enhanceCommandError(err) {
|
||||
let errorCode = tools.getStatusCode(err.response);
|
||||
if (errorCode) {
|
||||
err.serverResponseCode = errorCode;
|
||||
}
|
||||
err.response = await tools.getErrorText(err.response);
|
||||
return err;
|
||||
},
|
||||
|
||||
getFolderTree(folders) {
|
||||
let tree = {
|
||||
root: true,
|
||||
folders: []
|
||||
};
|
||||
|
||||
let getTreeNode = parents => {
|
||||
let node = tree;
|
||||
if (!parents || !parents.length) {
|
||||
return node;
|
||||
}
|
||||
|
||||
for (let parent of parents) {
|
||||
let cur = node.folders && node.folders.find(folder => folder.name === parent);
|
||||
if (cur) {
|
||||
node = cur;
|
||||
} else {
|
||||
// not yet set
|
||||
cur = {
|
||||
name: parent,
|
||||
folders: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
for (let folder of folders) {
|
||||
let parent = getTreeNode(folder.parent);
|
||||
// see if entry already exists
|
||||
let existing = parent.folders && parent.folders.find(existing => existing.name === folder.name);
|
||||
if (existing) {
|
||||
// update values
|
||||
existing.name = folder.name;
|
||||
existing.flags = folder.flags;
|
||||
existing.path = folder.path;
|
||||
existing.subscribed = !!folder.subscribed;
|
||||
existing.listed = !!folder.listed;
|
||||
existing.status = !!folder.status;
|
||||
|
||||
if (folder.specialUse) {
|
||||
existing.specialUse = folder.specialUse;
|
||||
}
|
||||
|
||||
if (folder.flags.has('\\Noselect')) {
|
||||
existing.disabled = true;
|
||||
}
|
||||
if (folder.flags.has('\\HasChildren') && !existing.folders) {
|
||||
existing.folders = [];
|
||||
}
|
||||
} else {
|
||||
// create new
|
||||
let data = {
|
||||
name: folder.name,
|
||||
flags: folder.flags,
|
||||
path: folder.path,
|
||||
subscribed: !!folder.subscribed,
|
||||
listed: !!folder.listed,
|
||||
status: !!folder.status
|
||||
};
|
||||
|
||||
if (folder.delimiter) {
|
||||
data.delimiter = folder.delimiter;
|
||||
}
|
||||
|
||||
if (folder.specialUse) {
|
||||
data.specialUse = folder.specialUse;
|
||||
}
|
||||
|
||||
if (folder.flags.has('\\Noselect')) {
|
||||
data.disabled = true;
|
||||
}
|
||||
|
||||
if (folder.flags.has('\\HasChildren')) {
|
||||
data.folders = [];
|
||||
}
|
||||
|
||||
if (!parent.folders) {
|
||||
parent.folders = [];
|
||||
}
|
||||
parent.folders.push(data);
|
||||
}
|
||||
}
|
||||
|
||||
return tree;
|
||||
},
|
||||
|
||||
getFlagColor(flags) {
|
||||
if (!flags.has('\\Flagged')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bit0 = flags.has('$MailFlagBit0') ? 1 : 0;
|
||||
const bit1 = flags.has('$MailFlagBit1') ? 2 : 0;
|
||||
const bit2 = flags.has('$MailFlagBit2') ? 4 : 0;
|
||||
|
||||
const color = bit0 | bit1 | bit2; // eslint-disable-line no-bitwise
|
||||
|
||||
return FLAG_COLORS[color] || 'red'; // default to red for the unused \b111
|
||||
},
|
||||
|
||||
getColorFlags(color) {
|
||||
const colorCode = color ? FLAG_COLORS.indexOf((color || '').toString().toLowerCase().trim()) : null;
|
||||
if (colorCode < 0 && colorCode !== null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bits = [];
|
||||
bits[0] = colorCode & 1; // eslint-disable-line no-bitwise
|
||||
bits[1] = colorCode & 2; // eslint-disable-line no-bitwise
|
||||
bits[2] = colorCode & 4; // eslint-disable-line no-bitwise
|
||||
|
||||
let result = { add: colorCode ? ['\\Flagged'] : [], remove: colorCode ? [] : ['\\Flagged'] };
|
||||
|
||||
for (let i = 0; i < bits.length; i++) {
|
||||
if (bits[i]) {
|
||||
result.add.push(`$MailFlagBit${i}`);
|
||||
} else {
|
||||
result.remove.push(`$MailFlagBit${i}`);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
async formatMessageResponse(untagged, mailbox) {
|
||||
let map = {};
|
||||
|
||||
map.seq = Number(untagged.command);
|
||||
|
||||
let key;
|
||||
let attributes = (untagged.attributes && untagged.attributes[1]) || [];
|
||||
for (let i = 0, len = attributes.length; i < len; i++) {
|
||||
let attribute = attributes[i];
|
||||
if (i % 2 === 0) {
|
||||
key = (
|
||||
await compiler({
|
||||
attributes: [attribute]
|
||||
})
|
||||
)
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.replace(/<\d+(\.\d+)?>$/, '');
|
||||
continue;
|
||||
}
|
||||
if (typeof key !== 'string') {
|
||||
// should not happen
|
||||
continue;
|
||||
}
|
||||
|
||||
let getString = attribute => {
|
||||
if (!attribute) {
|
||||
return false;
|
||||
}
|
||||
if (typeof attribute.value === 'string') {
|
||||
return attribute.value;
|
||||
}
|
||||
if (Buffer.isBuffer(attribute.value)) {
|
||||
return attribute.value.toString();
|
||||
}
|
||||
};
|
||||
|
||||
let getBuffer = attribute => {
|
||||
if (!attribute) {
|
||||
return false;
|
||||
}
|
||||
if (Buffer.isBuffer(attribute.value)) {
|
||||
return attribute.value;
|
||||
}
|
||||
};
|
||||
|
||||
let getArray = attribute => {
|
||||
if (Array.isArray(attribute)) {
|
||||
return attribute.map(entry => (entry && typeof entry.value === 'string' ? entry.value : false)).filter(entry => entry);
|
||||
}
|
||||
};
|
||||
|
||||
switch (key) {
|
||||
case 'body[]':
|
||||
case 'binary[]':
|
||||
map.source = getBuffer(attribute);
|
||||
break;
|
||||
|
||||
case 'uid':
|
||||
map.uid = Number(getString(attribute));
|
||||
if (map.uid && (!mailbox.uidNext || mailbox.uidNext <= map.uid)) {
|
||||
// current uidNext seems to be outdated, bump it
|
||||
mailbox.uidNext = map.uid + 1;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'modseq':
|
||||
map.modseq = BigInt(getArray(attribute)[0]);
|
||||
if (map.modseq && (!mailbox.highestModseq || mailbox.highestModseq < map.modseq)) {
|
||||
// current highestModseq seems to be outdated, bump it
|
||||
mailbox.highestModseq = map.modseq;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'emailid':
|
||||
map.emailId = getArray(attribute)[0];
|
||||
break;
|
||||
|
||||
case 'x-gm-msgid':
|
||||
map.emailId = getString(attribute);
|
||||
break;
|
||||
|
||||
case 'threadid':
|
||||
map.threadId = getArray(attribute)[0];
|
||||
break;
|
||||
|
||||
case 'x-gm-thrid':
|
||||
map.threadId = getString(attribute);
|
||||
break;
|
||||
|
||||
case 'x-gm-labels':
|
||||
map.labels = new Set(getArray(attribute));
|
||||
break;
|
||||
|
||||
case 'rfc822.size':
|
||||
map.size = Number(getString(attribute)) || 0;
|
||||
break;
|
||||
|
||||
case 'flags':
|
||||
map.flags = new Set(getArray(attribute));
|
||||
break;
|
||||
|
||||
case 'envelope':
|
||||
map.envelope = tools.parseEnvelope(attribute);
|
||||
break;
|
||||
|
||||
case 'bodystructure':
|
||||
map.bodyStructure = tools.parseBodystructure(attribute);
|
||||
break;
|
||||
|
||||
case 'internaldate': {
|
||||
let value = getString(attribute);
|
||||
let date = new Date(value);
|
||||
if (date.toString() === 'Invalid Date') {
|
||||
map.internalDate = value;
|
||||
} else {
|
||||
map.internalDate = date;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
let match = key.match(/(body|binary)\[/i);
|
||||
if (match) {
|
||||
let partKey = key.replace(/^(body|binary)\[|]$/gi, '');
|
||||
partKey = partKey.replace(/\.fields.*$/g, '');
|
||||
|
||||
let value = getBuffer(attribute);
|
||||
if (partKey === 'header') {
|
||||
map.headers = value;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!map.bodyParts) {
|
||||
map.bodyParts = new Map();
|
||||
}
|
||||
map.bodyParts.set(partKey, value);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (map.emailId || map.uid) {
|
||||
// define account unique ID for this email
|
||||
|
||||
// normalize path to use ascii, so we would always get the same ID
|
||||
let path = mailbox.path;
|
||||
if (/[0x80-0xff]/.test(path)) {
|
||||
try {
|
||||
path = iconv.encode(path, 'utf-7-imap').toString();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
map.id =
|
||||
map.emailId ||
|
||||
createHash('md5')
|
||||
.update([path, mailbox.uidValidity?.toString() || '', map.uid.toString()].join(':'))
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
if (map.flags) {
|
||||
let flagColor = tools.getFlagColor(map.flags);
|
||||
if (flagColor) {
|
||||
map.flagColor = flagColor;
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
},
|
||||
|
||||
processName(name) {
|
||||
name = (name || '').toString();
|
||||
if (name.length > 2 && name.at(0) === '"' && name.at(-1) === '"') {
|
||||
name = name.replace(/^"|"$/g, '');
|
||||
}
|
||||
return name;
|
||||
},
|
||||
|
||||
parseEnvelope(entry) {
|
||||
let getStrValue = obj => {
|
||||
if (!obj) {
|
||||
return false;
|
||||
}
|
||||
if (typeof obj.value === 'string') {
|
||||
return obj.value;
|
||||
}
|
||||
if (Buffer.isBuffer(obj.value)) {
|
||||
return obj.value.toString();
|
||||
}
|
||||
return obj.value;
|
||||
};
|
||||
|
||||
let processAddresses = function (list) {
|
||||
return []
|
||||
.concat(list || [])
|
||||
.map(addr => {
|
||||
let address = (getStrValue(addr[2]) || '') + '@' + (getStrValue(addr[3]) || '');
|
||||
if (address === '@') {
|
||||
address = '';
|
||||
}
|
||||
return {
|
||||
name: tools.processName(libmime.decodeWords(getStrValue(addr[0]))),
|
||||
address
|
||||
};
|
||||
})
|
||||
.filter(addr => addr.name || addr.address);
|
||||
},
|
||||
envelope = {};
|
||||
|
||||
if (entry[0] && entry[0].value) {
|
||||
let date = new Date(getStrValue(entry[0]));
|
||||
if (date.toString() === 'Invalid Date') {
|
||||
envelope.date = getStrValue(entry[0]);
|
||||
} else {
|
||||
envelope.date = date;
|
||||
}
|
||||
}
|
||||
|
||||
if (entry[1] && entry[1].value) {
|
||||
envelope.subject = libmime.decodeWords(getStrValue(entry[1]));
|
||||
}
|
||||
|
||||
if (entry[2] && entry[2].length) {
|
||||
envelope.from = processAddresses(entry[2]);
|
||||
}
|
||||
|
||||
if (entry[3] && entry[3].length) {
|
||||
envelope.sender = processAddresses(entry[3]);
|
||||
}
|
||||
|
||||
if (entry[4] && entry[4].length) {
|
||||
envelope.replyTo = processAddresses(entry[4]);
|
||||
}
|
||||
|
||||
if (entry[5] && entry[5].length) {
|
||||
envelope.to = processAddresses(entry[5]);
|
||||
}
|
||||
|
||||
if (entry[6] && entry[6].length) {
|
||||
envelope.cc = processAddresses(entry[6]);
|
||||
}
|
||||
|
||||
if (entry[7] && entry[7].length) {
|
||||
envelope.bcc = processAddresses(entry[7]);
|
||||
}
|
||||
|
||||
if (entry[8] && entry[8].value) {
|
||||
envelope.inReplyTo = (getStrValue(entry[8]) || '').toString().trim();
|
||||
}
|
||||
|
||||
if (entry[9] && entry[9].value) {
|
||||
envelope.messageId = (getStrValue(entry[9]) || '').toString().trim();
|
||||
}
|
||||
|
||||
return envelope;
|
||||
},
|
||||
|
||||
getStructuredParams(arr) {
|
||||
let key;
|
||||
|
||||
let params = {};
|
||||
|
||||
[].concat(arr || []).forEach((val, j) => {
|
||||
if (j % 2) {
|
||||
params[key] = libmime.decodeWords(((val && val.value) || '').toString());
|
||||
} else {
|
||||
key = ((val && val.value) || '').toString().toLowerCase();
|
||||
}
|
||||
});
|
||||
|
||||
if (params.filename && !params['filename*'] && /^[a-z\-_0-9]+'[a-z]*'[^'\x00-\x08\x0b\x0c\x0e-\x1f\u0080-\uFFFF]+/.test(params.filename)) {
|
||||
// seems like encoded value
|
||||
let [encoding, , encodedValue] = params.filename.split("'");
|
||||
if (resolveCharset(encoding)) {
|
||||
params['filename*'] = `${encoding}''${encodedValue}`;
|
||||
}
|
||||
}
|
||||
|
||||
// preprocess values
|
||||
Object.keys(params).forEach(key => {
|
||||
let actualKey;
|
||||
let nr;
|
||||
let value;
|
||||
|
||||
let match = key.match(/\*((\d+)\*?)?$/);
|
||||
|
||||
if (!match) {
|
||||
// nothing to do here, does not seem like a continuation param
|
||||
return;
|
||||
}
|
||||
|
||||
actualKey = key.substr(0, match.index).toLowerCase();
|
||||
nr = Number(match[2]) || 0;
|
||||
|
||||
if (!params[actualKey] || typeof params[actualKey] !== 'object') {
|
||||
params[actualKey] = {
|
||||
charset: false,
|
||||
values: []
|
||||
};
|
||||
}
|
||||
|
||||
value = params[key];
|
||||
|
||||
if (nr === 0 && match[0].charAt(match[0].length - 1) === '*' && (match = value.match(/^([^']*)'[^']*'(.*)$/))) {
|
||||
params[actualKey].charset = match[1] || 'utf-8';
|
||||
value = match[2];
|
||||
}
|
||||
|
||||
params[actualKey].values.push({ nr, value });
|
||||
|
||||
// remove the old reference
|
||||
delete params[key];
|
||||
});
|
||||
|
||||
// concatenate split rfc2231 strings and convert encoded strings to mime encoded words
|
||||
Object.keys(params).forEach(key => {
|
||||
let value;
|
||||
if (params[key] && Array.isArray(params[key].values)) {
|
||||
value = params[key].values
|
||||
.sort((a, b) => a.nr - b.nr)
|
||||
.map(val => (val && val.value) || '')
|
||||
.join('');
|
||||
|
||||
if (params[key].charset) {
|
||||
// convert "%AB" to "=?charset?Q?=AB?=" and then to unicode
|
||||
params[key] = libmime.decodeWords(
|
||||
'=?' +
|
||||
params[key].charset +
|
||||
'?Q?' +
|
||||
value
|
||||
// fix invalidly encoded chars
|
||||
.replace(/[=?_\s]/g, s => {
|
||||
let c = s.charCodeAt(0).toString(16);
|
||||
if (s === ' ') {
|
||||
return '_';
|
||||
} else {
|
||||
return '%' + (c.length < 2 ? '0' : '') + c;
|
||||
}
|
||||
})
|
||||
// change from urlencoding to percent encoding
|
||||
.replace(/%/g, '=') +
|
||||
'?='
|
||||
);
|
||||
} else {
|
||||
params[key] = libmime.decodeWords(value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return params;
|
||||
},
|
||||
|
||||
parseBodystructure(entry) {
|
||||
let walk = (node, path) => {
|
||||
path = path || [];
|
||||
|
||||
let curNode = {},
|
||||
i = 0,
|
||||
part = 0;
|
||||
|
||||
if (path.length) {
|
||||
curNode.part = path.join('.');
|
||||
}
|
||||
|
||||
// multipart
|
||||
if (Array.isArray(node[0])) {
|
||||
curNode.childNodes = [];
|
||||
while (Array.isArray(node[i])) {
|
||||
curNode.childNodes.push(walk(node[i], path.concat(++part)));
|
||||
i++;
|
||||
}
|
||||
|
||||
// multipart type
|
||||
curNode.type = 'multipart/' + ((node[i++] || {}).value || '').toString().toLowerCase();
|
||||
|
||||
// extension data (not available for BODY requests)
|
||||
|
||||
// body parameter parenthesized list
|
||||
if (i < node.length - 1) {
|
||||
if (node[i]) {
|
||||
curNode.parameters = tools.getStructuredParams(node[i]);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
} else {
|
||||
// content type
|
||||
curNode.type = [((node[i++] || {}).value || '').toString().toLowerCase(), ((node[i++] || {}).value || '').toString().toLowerCase()].join('/');
|
||||
|
||||
// body parameter parenthesized list
|
||||
if (node[i]) {
|
||||
curNode.parameters = tools.getStructuredParams(node[i]);
|
||||
}
|
||||
i++;
|
||||
|
||||
// id
|
||||
if (node[i]) {
|
||||
curNode.id = ((node[i] || {}).value || '').toString();
|
||||
}
|
||||
i++;
|
||||
|
||||
// description
|
||||
if (node[i]) {
|
||||
curNode.description = ((node[i] || {}).value || '').toString();
|
||||
}
|
||||
i++;
|
||||
|
||||
// encoding
|
||||
if (node[i]) {
|
||||
curNode.encoding = ((node[i] || {}).value || '').toString().toLowerCase();
|
||||
}
|
||||
i++;
|
||||
|
||||
// size
|
||||
if (node[i]) {
|
||||
curNode.size = Number((node[i] || {}).value || 0) || 0;
|
||||
}
|
||||
i++;
|
||||
|
||||
if (curNode.type === 'message/rfc822') {
|
||||
// message/rfc adds additional envelope, bodystructure and line count values
|
||||
|
||||
// envelope
|
||||
if (node[i]) {
|
||||
curNode.envelope = tools.parseEnvelope([].concat(node[i] || []));
|
||||
}
|
||||
i++;
|
||||
|
||||
if (node[i]) {
|
||||
curNode.childNodes = [
|
||||
// rfc822 bodyparts share the same path, difference is between MIME and HEADER
|
||||
// path.MIME returns message/rfc822 header
|
||||
// path.HEADER returns inlined message header
|
||||
walk(node[i], path)
|
||||
];
|
||||
}
|
||||
i++;
|
||||
|
||||
// line count
|
||||
if (node[i]) {
|
||||
curNode.lineCount = Number((node[i] || {}).value || 0) || 0;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
if (/^text\//.test(curNode.type)) {
|
||||
// text/* adds additional line count value
|
||||
|
||||
// NB! some less known servers do not include the line count value
|
||||
// length should be 12+
|
||||
if (node.length === 11 && Array.isArray(node[i + 1]) && !Array.isArray(node[i + 2])) {
|
||||
// invalid structure, disposition params are shifted
|
||||
} else {
|
||||
// correct structure, line count number is provided
|
||||
if (node[i]) {
|
||||
// line count
|
||||
curNode.lineCount = Number((node[i] || {}).value || 0) || 0;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
// extension data (not available for BODY requests)
|
||||
|
||||
// md5
|
||||
if (i < node.length - 1) {
|
||||
if (node[i]) {
|
||||
curNode.md5 = ((node[i] || {}).value || '').toString().toLowerCase();
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
// the following are shared extension values (for both multipart and non-multipart parts)
|
||||
// not available for BODY requests
|
||||
|
||||
// body disposition
|
||||
if (i < node.length - 1) {
|
||||
if (Array.isArray(node[i]) && node[i].length) {
|
||||
curNode.disposition = ((node[i][0] || {}).value || '').toString().toLowerCase();
|
||||
if (Array.isArray(node[i][1])) {
|
||||
curNode.dispositionParameters = tools.getStructuredParams(node[i][1]);
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
// body language
|
||||
if (i < node.length - 1) {
|
||||
if (node[i]) {
|
||||
curNode.language = [].concat(node[i] || []).map(val => ((val && val.value) || '').toString().toLowerCase());
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
// body location
|
||||
// NB! defined as a "string list" in RFC3501 but replaced in errata document with "string"
|
||||
// Errata: http://www.rfc-editor.org/errata_search.php?rfc=3501
|
||||
if (i < node.length - 1) {
|
||||
if (node[i]) {
|
||||
curNode.location = ((node[i] || {}).value || '').toString();
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
return curNode;
|
||||
};
|
||||
|
||||
return walk(entry);
|
||||
},
|
||||
|
||||
isDate(obj) {
|
||||
return Object.prototype.toString.call(obj) === '[object Date]';
|
||||
},
|
||||
|
||||
toValidDate(value) {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
value = new Date(value);
|
||||
}
|
||||
if (!tools.isDate(value) || value.toString() === 'Invalid Date') {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
|
||||
formatDate(value) {
|
||||
value = tools.toValidDate(value);
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
let dateParts = value.toISOString().substr(0, 10).split('-');
|
||||
dateParts.reverse();
|
||||
|
||||
let months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
dateParts[1] = months[Number(dateParts[1]) - 1];
|
||||
|
||||
return dateParts.join('-');
|
||||
},
|
||||
|
||||
formatDateTime(value) {
|
||||
value = tools.toValidDate(value);
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
let dateStr = tools.formatDate(value).replace(/^0/, ' '); //starts with date-day-fixed with leading 0 replaced by SP
|
||||
let timeStr = value.toISOString().substr(11, 8);
|
||||
|
||||
return `${dateStr} ${timeStr} +0000`;
|
||||
},
|
||||
|
||||
formatFlag(flag) {
|
||||
switch (flag.toLowerCase()) {
|
||||
case '\\recent':
|
||||
// can not set or remove
|
||||
return false;
|
||||
case '\\seen':
|
||||
case '\\answered':
|
||||
case '\\flagged':
|
||||
case '\\deleted':
|
||||
case '\\draft':
|
||||
// can not set or remove
|
||||
return flag.toLowerCase().replace(/^\\./, c => c.toUpperCase());
|
||||
}
|
||||
return flag;
|
||||
},
|
||||
|
||||
canUseFlag(mailbox, flag) {
|
||||
return !mailbox || !mailbox.permanentFlags || mailbox.permanentFlags.has('\\*') || mailbox.permanentFlags.has(flag);
|
||||
},
|
||||
|
||||
expandRange(range) {
|
||||
return range.split(',').flatMap(entry => {
|
||||
entry = entry.trim();
|
||||
let colon = entry.indexOf(':');
|
||||
if (colon < 0) {
|
||||
return Number(entry) || 0;
|
||||
}
|
||||
let first = Number(entry.substr(0, colon)) || 0;
|
||||
let second = Number(entry.substr(colon + 1)) || 0;
|
||||
if (first === second) {
|
||||
return first;
|
||||
}
|
||||
let list = [];
|
||||
if (first < second) {
|
||||
for (let i = first; i <= second; i++) {
|
||||
list.push(i);
|
||||
}
|
||||
} else {
|
||||
for (let i = first; i >= second; i--) {
|
||||
list.push(i);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
});
|
||||
},
|
||||
|
||||
getDecoder(charset) {
|
||||
charset = (charset || 'ascii').toString().trim().toLowerCase();
|
||||
if (/^jis|^iso-?2022-?jp|^EUCJP/i.test(charset)) {
|
||||
// special case not supported by iconv-lite
|
||||
return new JPDecoder(charset);
|
||||
}
|
||||
|
||||
return iconv.decodeStream(charset);
|
||||
},
|
||||
|
||||
packMessageRange(list) {
|
||||
if (!Array.isArray(list)) {
|
||||
list = [].concat(list || []);
|
||||
}
|
||||
|
||||
if (!list.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
list.sort((a, b) => a - b);
|
||||
|
||||
let last = list[list.length - 1];
|
||||
let result = [[last]];
|
||||
for (let i = list.length - 2; i >= 0; i--) {
|
||||
if (list[i] === list[i + 1] - 1) {
|
||||
result[0].unshift(list[i]);
|
||||
continue;
|
||||
}
|
||||
result.unshift([list[i]]);
|
||||
}
|
||||
|
||||
result = result.map(item => {
|
||||
if (item.length === 1) {
|
||||
return item[0];
|
||||
}
|
||||
return item.shift() + ':' + item.pop();
|
||||
});
|
||||
|
||||
return result.join(',');
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = tools;
|
||||
1
backend/node_modules/imapflow/node_modules/.bin/pino
generated
vendored
Symbolic link
1
backend/node_modules/imapflow/node_modules/.bin/pino
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../pino/bin.js
|
||||
6
backend/node_modules/imapflow/node_modules/nodemailer/.gitattributes
generated
vendored
Normal file
6
backend/node_modules/imapflow/node_modules/nodemailer/.gitattributes
generated
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
*.js text eol=lf
|
||||
*.txt text eol=lf
|
||||
*.html text eol=lf
|
||||
*.htm text eol=lf
|
||||
*.ics -text
|
||||
*.bin -text
|
||||
9
backend/node_modules/imapflow/node_modules/nodemailer/.ncurc.js
generated
vendored
Normal file
9
backend/node_modules/imapflow/node_modules/nodemailer/.ncurc.js
generated
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
upgrade: true,
|
||||
reject: [
|
||||
// API changes break existing tests
|
||||
'proxy'
|
||||
]
|
||||
};
|
||||
8
backend/node_modules/imapflow/node_modules/nodemailer/.prettierignore
generated
vendored
Normal file
8
backend/node_modules/imapflow/node_modules/nodemailer/.prettierignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
node_modules
|
||||
coverage
|
||||
*.min.js
|
||||
dist
|
||||
build
|
||||
.nyc_output
|
||||
package-lock.json
|
||||
CHANGELOG.md
|
||||
12
backend/node_modules/imapflow/node_modules/nodemailer/.prettierrc
generated
vendored
Normal file
12
backend/node_modules/imapflow/node_modules/nodemailer/.prettierrc
generated
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"printWidth": 140,
|
||||
"tabWidth": 4,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"quoteProps": "as-needed",
|
||||
"trailingComma": "none",
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "avoid",
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
10
backend/node_modules/imapflow/node_modules/nodemailer/.prettierrc.js
generated
vendored
Normal file
10
backend/node_modules/imapflow/node_modules/nodemailer/.prettierrc.js
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
printWidth: 160,
|
||||
tabWidth: 4,
|
||||
singleQuote: true,
|
||||
endOfLine: 'lf',
|
||||
trailingComma: 'none',
|
||||
arrowParens: 'avoid'
|
||||
};
|
||||
9
backend/node_modules/imapflow/node_modules/nodemailer/.release-please-config.json
generated
vendored
Normal file
9
backend/node_modules/imapflow/node_modules/nodemailer/.release-please-config.json
generated
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"packages": {
|
||||
".": {
|
||||
"release-type": "node",
|
||||
"package-name": "nodemailer",
|
||||
"pull-request-title-pattern": "chore${scope}: release ${version} [skip-ci]"
|
||||
}
|
||||
}
|
||||
}
|
||||
929
backend/node_modules/imapflow/node_modules/nodemailer/CHANGELOG.md
generated
vendored
Normal file
929
backend/node_modules/imapflow/node_modules/nodemailer/CHANGELOG.md
generated
vendored
Normal file
@@ -0,0 +1,929 @@
|
||||
# CHANGELOG
|
||||
|
||||
## [7.0.12](https://github.com/nodemailer/nodemailer/compare/v7.0.11...v7.0.12) (2025-12-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* added support for REQUIRETLS ([#1793](https://github.com/nodemailer/nodemailer/issues/1793)) ([053ce6a](https://github.com/nodemailer/nodemailer/commit/053ce6a772a7c608e6bee7f58ebe9900afbd9b84))
|
||||
* use 8bit encoding for message/rfc822 attachments ([adf8611](https://github.com/nodemailer/nodemailer/commit/adf86113217b23ff3cd1191af5cd1d360fcc313b))
|
||||
|
||||
## [7.0.11](https://github.com/nodemailer/nodemailer/compare/v7.0.10...v7.0.11) (2025-11-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* prevent stack overflow DoS in addressparser with deeply nested groups ([b61b9c0](https://github.com/nodemailer/nodemailer/commit/b61b9c0cfd682b6f647754ca338373b68336a150))
|
||||
|
||||
## [7.0.10](https://github.com/nodemailer/nodemailer/compare/v7.0.9...v7.0.10) (2025-10-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Increase data URI size limit from 100KB to 50MB and preserve content type ([28dbf3f](https://github.com/nodemailer/nodemailer/commit/28dbf3fe129653f5756c150a98dc40593bfb2cfe))
|
||||
|
||||
## [7.0.9](https://github.com/nodemailer/nodemailer/compare/v7.0.8...v7.0.9) (2025-10-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **release:** Trying to fix release proecess by upgrading Node version in runner ([579fce4](https://github.com/nodemailer/nodemailer/commit/579fce4683eb588891613a6c9a00d8092e8c62d1))
|
||||
|
||||
## [7.0.8](https://github.com/nodemailer/nodemailer/compare/v7.0.7...v7.0.8) (2025-10-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **addressparser:** flatten nested groups per RFC 5322 ([8f8a77c](https://github.com/nodemailer/nodemailer/commit/8f8a77c67f0ba94ddf4e16c68f604a5920fb5d26))
|
||||
|
||||
## [7.0.7](https://github.com/nodemailer/nodemailer/compare/v7.0.6...v7.0.7) (2025-10-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **addressparser:** Fixed addressparser handling of quoted nested email addresses ([1150d99](https://github.com/nodemailer/nodemailer/commit/1150d99fba77280df2cfb1885c43df23109a8626))
|
||||
- **dns:** add memory leak prevention for DNS cache ([0240d67](https://github.com/nodemailer/nodemailer/commit/0240d6795ded6d8008d102161a729f120b6d786a))
|
||||
- **linter:** Updated eslint and created prettier formatting task ([df13b74](https://github.com/nodemailer/nodemailer/commit/df13b7487e368acded35e45d0887d23c89c9177a))
|
||||
- refresh expired DNS cache on error ([#1759](https://github.com/nodemailer/nodemailer/issues/1759)) ([ea0fc5a](https://github.com/nodemailer/nodemailer/commit/ea0fc5a6633a3546f4b00fcf2f428e9ca732cdb6))
|
||||
- resolve linter errors in DNS cache tests ([3b8982c](https://github.com/nodemailer/nodemailer/commit/3b8982c1f24508089a8757b74039000a4498b158))
|
||||
|
||||
## [7.0.6](https://github.com/nodemailer/nodemailer/compare/v7.0.5...v7.0.6) (2025-08-27)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **encoder:** avoid silent data loss by properly flushing trailing base64 ([#1747](https://github.com/nodemailer/nodemailer/issues/1747)) ([01ae76f](https://github.com/nodemailer/nodemailer/commit/01ae76f2cfe991c0c3fe80170f236da60531496b))
|
||||
- handle multiple XOAUTH2 token requests correctly ([#1754](https://github.com/nodemailer/nodemailer/issues/1754)) ([dbe0028](https://github.com/nodemailer/nodemailer/commit/dbe00286351cddf012726a41a96ae613d30a34ee))
|
||||
- ReDoS vulnerability in parseDataURI and \_processDataUrl ([#1755](https://github.com/nodemailer/nodemailer/issues/1755)) ([90b3e24](https://github.com/nodemailer/nodemailer/commit/90b3e24d23929ebf9f4e16261049b40ee4055a39))
|
||||
|
||||
## [7.0.5](https://github.com/nodemailer/nodemailer/compare/v7.0.4...v7.0.5) (2025-07-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- updated well known delivery service list ([fa2724b](https://github.com/nodemailer/nodemailer/commit/fa2724b337eb8d8fdcdd788fe903980b061316b8))
|
||||
|
||||
## [7.0.4](https://github.com/nodemailer/nodemailer/compare/v7.0.3...v7.0.4) (2025-06-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **pools:** Emit 'clear' once transporter is idle and all connections are closed ([839e286](https://github.com/nodemailer/nodemailer/commit/839e28634c9a93ae4321f399a8c893bf487a09fa))
|
||||
- **smtp-connection:** jsdoc public annotation for socket ([#1741](https://github.com/nodemailer/nodemailer/issues/1741)) ([c45c84f](https://github.com/nodemailer/nodemailer/commit/c45c84fe9b8e2ec5e0615ab02d4197473911ab3e))
|
||||
- **well-known-services:** Added AliyunQiye ([bb9e6da](https://github.com/nodemailer/nodemailer/commit/bb9e6daffb632d7d8f969359859f88a138de3a48))
|
||||
|
||||
## [7.0.3](https://github.com/nodemailer/nodemailer/compare/v7.0.2...v7.0.3) (2025-05-08)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **attachments:** Set the default transfer encoding for message/rfc822 attachments as '7bit' ([007d5f3](https://github.com/nodemailer/nodemailer/commit/007d5f3f40908c588f1db46c76de8b64ff429327))
|
||||
|
||||
## [7.0.2](https://github.com/nodemailer/nodemailer/compare/v7.0.1...v7.0.2) (2025-05-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **ses:** Fixed structured from header ([faa9a5e](https://github.com/nodemailer/nodemailer/commit/faa9a5eafaacbaf85de3540466a04636e12729b3))
|
||||
|
||||
## [7.0.1](https://github.com/nodemailer/nodemailer/compare/v7.0.0...v7.0.1) (2025-05-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **ses:** Use formatted FromEmailAddress for SES emails ([821cd09](https://github.com/nodemailer/nodemailer/commit/821cd09002f16c20369cc728b9414c7eb99e4113))
|
||||
|
||||
## [7.0.0](https://github.com/nodemailer/nodemailer/compare/v6.10.1...v7.0.0) (2025-05-03)
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
- SESv2 SDK support, removed older SES SDK v2 and v3 , removed SES rate limiting and idling features
|
||||
|
||||
### Features
|
||||
|
||||
- SESv2 SDK support, removed older SES SDK v2 and v3 , removed SES rate limiting and idling features ([15db667](https://github.com/nodemailer/nodemailer/commit/15db667af2d0a5ed835281cfdbab16ee73b5edce))
|
||||
|
||||
## [6.10.1](https://github.com/nodemailer/nodemailer/compare/v6.10.0...v6.10.1) (2025-02-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- close correct socket ([a18062c](https://github.com/nodemailer/nodemailer/commit/a18062c04d0e05ca4357fbe8f0a59b690fa5391e))
|
||||
|
||||
## [6.10.0](https://github.com/nodemailer/nodemailer/compare/v6.9.16...v6.10.0) (2025-01-23)
|
||||
|
||||
### Features
|
||||
|
||||
- **services:** add Seznam email service configuration ([#1695](https://github.com/nodemailer/nodemailer/issues/1695)) ([d1ae0a8](https://github.com/nodemailer/nodemailer/commit/d1ae0a86883ba6011a49a5bbdf076098e2e3637a))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **proxy:** Set error and timeout errors for proxied sockets ([aa0c99c](https://github.com/nodemailer/nodemailer/commit/aa0c99c8f25440bb3dc91f4f3448777c800604d7))
|
||||
|
||||
## [6.9.16](https://github.com/nodemailer/nodemailer/compare/v6.9.15...v6.9.16) (2024-10-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **addressparser:** Correctly detect if user local part is attached to domain part ([f2096c5](https://github.com/nodemailer/nodemailer/commit/f2096c51b92a69ecfbcc15884c28cb2c2f00b826))
|
||||
|
||||
## [6.9.15](https://github.com/nodemailer/nodemailer/compare/v6.9.14...v6.9.15) (2024-08-08)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix memory leak ([#1667](https://github.com/nodemailer/nodemailer/issues/1667)) ([baa28f6](https://github.com/nodemailer/nodemailer/commit/baa28f659641a4bc30360633673d851618f8e8bd))
|
||||
- **mime:** Added GeoJSON closes [#1637](https://github.com/nodemailer/nodemailer/issues/1637) ([#1665](https://github.com/nodemailer/nodemailer/issues/1665)) ([79b8293](https://github.com/nodemailer/nodemailer/commit/79b8293ad557d36f066b4675e649dd80362fd45b))
|
||||
|
||||
## [6.9.14](https://github.com/nodemailer/nodemailer/compare/v6.9.13...v6.9.14) (2024-06-19)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **api:** Added support for Ethereal authentication ([56b2205](https://github.com/nodemailer/nodemailer/commit/56b22052a98de9e363f6c4d26d1512925349c3f3))
|
||||
- **services.json:** Add Email Services Provider Feishu Mail (CN) ([#1648](https://github.com/nodemailer/nodemailer/issues/1648)) ([e9e9ecc](https://github.com/nodemailer/nodemailer/commit/e9e9ecc99b352948a912868c7912b280a05178c6))
|
||||
- **services.json:** update Mailtrap host and port in well known ([#1652](https://github.com/nodemailer/nodemailer/issues/1652)) ([fc2c9ea](https://github.com/nodemailer/nodemailer/commit/fc2c9ea0b4c4f4e514143d2a138c9a23095fc827))
|
||||
- **well-known-services:** Add Loopia in well known services ([#1655](https://github.com/nodemailer/nodemailer/issues/1655)) ([21a28a1](https://github.com/nodemailer/nodemailer/commit/21a28a18fc9fdf8e0e86ddd846e54641395b2cb6))
|
||||
|
||||
## [6.9.13](https://github.com/nodemailer/nodemailer/compare/v6.9.12...v6.9.13) (2024-03-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **tls:** Ensure servername for SMTP ([d66fdd3](https://github.com/nodemailer/nodemailer/commit/d66fdd3dccacc4bc79d697fe9009204cc8d4bde0))
|
||||
|
||||
## [6.9.12](https://github.com/nodemailer/nodemailer/compare/v6.9.11...v6.9.12) (2024-03-08)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **message-generation:** Escape single quote in address names ([4ae5fad](https://github.com/nodemailer/nodemailer/commit/4ae5fadeaac70ba91abf529fcaae65f829a39101))
|
||||
|
||||
## [6.9.11](https://github.com/nodemailer/nodemailer/compare/v6.9.10...v6.9.11) (2024-02-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **headers:** Ensure that Content-type is the bottom header ([c7cf97e](https://github.com/nodemailer/nodemailer/commit/c7cf97e5ecc83f8eee773359951df995c9945446))
|
||||
|
||||
## [6.9.10](https://github.com/nodemailer/nodemailer/compare/v6.9.9...v6.9.10) (2024-02-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **data-uri:** Do not use regular expressions for parsing data URI schemes ([12e65e9](https://github.com/nodemailer/nodemailer/commit/12e65e975d80efe6bafe6de4590829b3b5ebb492))
|
||||
- **data-uri:** Moved all data-uri regexes to use the non-regex parseDataUri method ([edd5dfe](https://github.com/nodemailer/nodemailer/commit/edd5dfe5ce9b725f8b8ae2830797f65b2a2b0a33))
|
||||
|
||||
## [6.9.9](https://github.com/nodemailer/nodemailer/compare/v6.9.8...v6.9.9) (2024-02-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **security:** Fix issues described in GHSA-9h6g-pr28-7cqp. Do not use eternal matching pattern if only a few occurences are expected ([dd8f5e8](https://github.com/nodemailer/nodemailer/commit/dd8f5e8a4ddc99992e31df76bcff9c590035cd4a))
|
||||
- **tests:** Use native node test runner, added code coverage support, removed grunt ([#1604](https://github.com/nodemailer/nodemailer/issues/1604)) ([be45c1b](https://github.com/nodemailer/nodemailer/commit/be45c1b299d012358d69247019391a02734d70af))
|
||||
|
||||
## [6.9.8](https://github.com/nodemailer/nodemailer/compare/v6.9.7...v6.9.8) (2023-12-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **punycode:** do not use native punycode module ([b4d0e0c](https://github.com/nodemailer/nodemailer/commit/b4d0e0c7cc4b15bc4d9e287f91d1bcaca87508b0))
|
||||
|
||||
## [6.9.7](https://github.com/nodemailer/nodemailer/compare/v6.9.6...v6.9.7) (2023-10-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **customAuth:** Do not require user and pass to be set for custom authentication schemes (fixes [#1584](https://github.com/nodemailer/nodemailer/issues/1584)) ([41d482c](https://github.com/nodemailer/nodemailer/commit/41d482c3f01e26111b06f3e46351b193db3fb5cb))
|
||||
|
||||
## [6.9.6](https://github.com/nodemailer/nodemailer/compare/v6.9.5...v6.9.6) (2023-10-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **inline:** Use 'inline' as the default Content Dispostion value for embedded images ([db32c93](https://github.com/nodemailer/nodemailer/commit/db32c93fefee527bcc239f13056e5d9181a4d8af))
|
||||
- **tests:** Removed Node v12 from test matrix as it is not compatible with the test framework anymore ([7fe0a60](https://github.com/nodemailer/nodemailer/commit/7fe0a608ed6bcb70dc6b2de543ebfc3a30abf984))
|
||||
|
||||
## [6.9.5](https://github.com/nodemailer/nodemailer/compare/v6.9.4...v6.9.5) (2023-09-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **license:** Updated license year ([da4744e](https://github.com/nodemailer/nodemailer/commit/da4744e491f3a68f4f68e4073684370592630e01))
|
||||
|
||||
## 6.9.4 2023-07-19
|
||||
|
||||
- Renamed SendinBlue to Brevo
|
||||
|
||||
## 6.9.3 2023-05-29
|
||||
|
||||
- Specified license identifier (was defined as MIT, actual value MIT-0)
|
||||
- If SMTP server disconnects with a message, process it and include as part of the response error
|
||||
|
||||
## 6.9.2 2023-05-11
|
||||
|
||||
- Fix uncaught exception on invalid attachment content payload
|
||||
|
||||
## 6.9.1 2023-01-27
|
||||
|
||||
- Fix base64 encoding for emoji bytes in encoded words
|
||||
|
||||
## 6.9.0 2023-01-12
|
||||
|
||||
- Do not throw if failed to resolve IPv4 addresses
|
||||
- Include EHLO extensions in the send response
|
||||
- fix sendMail function: callback should be optional
|
||||
|
||||
## 6.8.0 2022-09-28
|
||||
|
||||
- Add DNS timeout (huksley)
|
||||
- add dns.REFUSED (lucagianfelici)
|
||||
|
||||
## 6.7.8 2022-08-11
|
||||
|
||||
- Allow to use multiple Reply-To addresses
|
||||
|
||||
## 6.7.7 2022-07-06
|
||||
|
||||
- Resolver fixes
|
||||
|
||||
## 6.7.5 2022-05-04
|
||||
|
||||
- No changes, pushing a new README to npmjs.org
|
||||
|
||||
## 6.7.4 2022-04-29
|
||||
|
||||
- Ensure compatibility with Node 18
|
||||
- Replaced Travis with Github Actions
|
||||
|
||||
## 6.7.3 2022-03-21
|
||||
|
||||
- Typo fixes
|
||||
- Added stale issue automation fir Github
|
||||
- Add Infomaniak config to well known service (popod)
|
||||
- Update Outlook/Hotmail host in well known services (popod)
|
||||
- fix: DSN recipient gets ignored (KornKalle)
|
||||
|
||||
## 6.7.2 2021-11-26
|
||||
|
||||
- Fix proxies for account verification
|
||||
|
||||
## 6.7.1 2021-11-15
|
||||
|
||||
- fix verify on ses-transport (stanofsky)
|
||||
|
||||
## 6.7.0 2021-10-11
|
||||
|
||||
- Updated DNS resolving logic. If there are multiple responses for a A/AAAA record, then loop these randomly instead of only caching the first one
|
||||
|
||||
## 6.6.5 2021-09-23
|
||||
|
||||
- Replaced Object.values() and Array.flat() with polyfills to allow using Nodemailer in Node v6+
|
||||
|
||||
## 6.6.4 2021-09-22
|
||||
|
||||
- Better compatibility with IPv6-only SMTP hosts (oxzi)
|
||||
- Fix ses verify for sdk v3 (hannesvdvreken)
|
||||
- Added SECURITY.txt for contact info
|
||||
|
||||
## 6.6.3 2021-07-14
|
||||
|
||||
- Do not show passwords in SMTP transaction logs. All passwords used in logging are replaced by `"/* secret */"`
|
||||
|
||||
## 6.6.1 2021-05-23
|
||||
|
||||
- Fixed address formatting issue where newlines in an email address, if provided via address object, were not properly removed. Reported by tmazeika (#1289)
|
||||
|
||||
## 6.6.0 2021-04-28
|
||||
|
||||
- Added new option `newline` for MailComposer
|
||||
- aws ses connection verification (Ognjen Jevremovic)
|
||||
|
||||
## 6.5.0 2021-02-26
|
||||
|
||||
- Pass through textEncoding to subnodes
|
||||
- Added support for AWS SES v3 SDK
|
||||
- Fixed tests
|
||||
|
||||
## 6.4.18 2021-02-11
|
||||
|
||||
- Updated README
|
||||
|
||||
## 6.4.17 2020-12-11
|
||||
|
||||
- Allow mixing attachments with caendar alternatives
|
||||
|
||||
## 6.4.16 2020-11-12
|
||||
|
||||
- Applied updated prettier formating rules
|
||||
|
||||
## 6.4.15 2020-11-06
|
||||
|
||||
- Minor changes in header key casing
|
||||
|
||||
## 6.4.14 2020-10-14
|
||||
|
||||
- Disabled postinstall script
|
||||
|
||||
## 6.4.13 2020-10-02
|
||||
|
||||
- Fix normalizeHeaderKey method for single node messages
|
||||
|
||||
## 6.4.12 2020-09-30
|
||||
|
||||
- Better handling of attachment filenames that include quote symbols
|
||||
- Includes all information from the oath2 error response in the error message (Normal Gaussian) [1787f227]
|
||||
|
||||
## 6.4.11 2020-07-29
|
||||
|
||||
- Fixed escape sequence handling in address parsing
|
||||
|
||||
## 6.4.10 2020-06-17
|
||||
|
||||
- Fixed RFC822 output for MailComposer when using invalid content-type value. Mostly relevant if message attachments have stragne content-type values set.
|
||||
|
||||
## 6.4.7 2020-05-28
|
||||
|
||||
- Always set charset=utf-8 for Content-Type headers
|
||||
- Catch error when using invalid crypto.sign input
|
||||
|
||||
## 6.4.6 2020-03-20
|
||||
|
||||
- fix: `requeueAttempts=n` should requeue `n` times (Patrick Malouin) [a27ed2f7]
|
||||
|
||||
## 6.4.4 2020-03-01
|
||||
|
||||
- Add `options.forceAuth` for SMTP (Patrick Malouin) [a27ed2f7]
|
||||
|
||||
## 6.4.3 2020-02-22
|
||||
|
||||
- Added an option to specify max number of requeues when connection closes unexpectedly (Igor Sechyn) [8a927f5a]
|
||||
|
||||
## 6.4.2 2019-12-11
|
||||
|
||||
- Fixed bug where array item was used with a potentially empty array
|
||||
|
||||
## 6.4.1 2019-12-07
|
||||
|
||||
- Fix processing server output with unterminated responses
|
||||
|
||||
## 6.4.0 2019-12-04
|
||||
|
||||
- Do not use auth if server does not advertise AUTH support [f419b09d]
|
||||
- add dns.CONNREFUSED (Hiroyuki Okada) [5c4c8ca8]
|
||||
|
||||
## 6.3.1 2019-10-09
|
||||
|
||||
- Ignore "end" events because it might be "error" after it (dex4er) [72bade9]
|
||||
- Set username and password on the connection proxy object correctly (UsamaAshraf) [250b1a8]
|
||||
- Support more DNS errors (madarche) [2391aa4]
|
||||
|
||||
## 6.3.0 2019-07-14
|
||||
|
||||
- Added new option to pass a set of httpHeaders to be sent when fetching attachments. See [PR #1034](https://github.com/nodemailer/nodemailer/pull/1034)
|
||||
|
||||
## 6.2.1 2019-05-24
|
||||
|
||||
- No changes. It is the same as 6.2.0 that was accidentally published as 6.2.1 to npm
|
||||
|
||||
## 6.2.0 2019-05-24
|
||||
|
||||
- Added new option for addressparser: `flatten`. If true then ignores group names and returns a single list of all addresses
|
||||
|
||||
## 6.1.1 2019-04-20
|
||||
|
||||
- Fixed regression bug with missing smtp `authMethod` property
|
||||
|
||||
## 6.1.0 2019-04-06
|
||||
|
||||
- Added new message property `amp` for providing AMP4EMAIL content
|
||||
|
||||
## 6.0.0 2019-03-25
|
||||
|
||||
- SMTPConnection: use removeListener instead of removeAllListeners (xr0master) [ddc4af15]
|
||||
Using removeListener should fix memory leak with Node.js streams
|
||||
|
||||
## 5.1.1 2019-01-09
|
||||
|
||||
- Added missing option argument for custom auth
|
||||
|
||||
## 5.1.0 2019-01-09
|
||||
|
||||
- Official support for custom authentication methods and examples (examples/custom-auth-async.js and examples/custom-auth-cb.js)
|
||||
|
||||
## 5.0.1 2019-01-09
|
||||
|
||||
- Fixed regression error to support Node versions lower than 6.11
|
||||
- Added expiremental custom authentication support
|
||||
|
||||
## 5.0.0 2018-12-28
|
||||
|
||||
- Start using dns.resolve() instead of dns.lookup() for resolving SMTP hostnames. Might be breaking change on some environments so upgrade with care
|
||||
- Show more logs for renewing OAuth2 tokens, previously it was not possible to see what actually failed
|
||||
|
||||
## 4.7.0 2018-11-19
|
||||
|
||||
- Cleaned up List-\* header generation
|
||||
- Fixed 'full' return option for DSN (klaronix) [23b93a3b]
|
||||
- Support promises `for mailcomposer.build()`
|
||||
|
||||
## 4.6.8 2018-08-15
|
||||
|
||||
- Use first IP address from DNS resolution when using a proxy (Limbozz) [d4ca847c]
|
||||
- Return raw email from SES transport (gabegorelick) [3aa08967]
|
||||
|
||||
## 4.6.7 2018-06-15
|
||||
|
||||
- Added option `skipEncoding` to JSONTransport
|
||||
|
||||
## 4.6.6 2018-06-10
|
||||
|
||||
- Fixes mime encoded-word compatibility issue with invalid clients like Zimbra
|
||||
|
||||
## 4.6.5 2018-05-23
|
||||
|
||||
- Fixed broken DKIM stream in Node.js v10
|
||||
- Updated error messages for SMTP responses to not include a newline
|
||||
|
||||
## 4.6.4 2018-03-31
|
||||
|
||||
- Readded logo author link to README that was accidentally removed a while ago
|
||||
|
||||
## 4.6.3 2018-03-13
|
||||
|
||||
- Removed unneeded dependency
|
||||
|
||||
## 4.6.2 2018-03-06
|
||||
|
||||
- When redirecting URL calls then do not include original POST content
|
||||
|
||||
## 4.6.1 2018-03-06
|
||||
|
||||
- Fixed Smtp connection freezing, when trying to send after close / quit (twawszczak) [73d3911c]
|
||||
|
||||
## 4.6.0 2018-02-22
|
||||
|
||||
- Support socks module v2 in addition to v1 [e228bcb2]
|
||||
- Fixed invalid promise return value when using createTestAccount [5524e627]
|
||||
- Allow using local addresses [8f6fa35f]
|
||||
|
||||
## 4.5.0 2018-02-21
|
||||
|
||||
- Added new message transport option `normalizeHeaderKey(key)=>normalizedKey` for custom header formatting
|
||||
|
||||
## 4.4.2 2018-01-20
|
||||
|
||||
- Added sponsors section to README
|
||||
- enclose encodeURIComponent in try..catch to handle invalid urls
|
||||
|
||||
## 4.4.1 2017-12-08
|
||||
|
||||
- Better handling of unexpectedly dropping connections
|
||||
|
||||
## 4.4.0 2017-11-10
|
||||
|
||||
- Changed default behavior for attachment option contentTransferEncoding. If it is unset then base64 encoding is used for the attachment. If it is set to false then previous default applies (base64 for most, 7bit for text)
|
||||
|
||||
## 4.3.1 2017-10-25
|
||||
|
||||
- Fixed a confict with Electron.js where timers do not have unref method
|
||||
|
||||
## 4.3.0 2017-10-23
|
||||
|
||||
- Added new mail object method `mail.normalize(cb)` that should make creating HTTP API based transports much easier
|
||||
|
||||
## 4.2.0 2017-10-13
|
||||
|
||||
- Expose streamed messages size and timers in info response
|
||||
|
||||
## v4.1.3 2017-10-06
|
||||
|
||||
- Allow generating preview links without calling createTestAccount first
|
||||
|
||||
## v4.1.2 2017-10-03
|
||||
|
||||
- No actual changes. Needed to push updated README to npmjs
|
||||
|
||||
## v4.1.1 2017-09-25
|
||||
|
||||
- Fixed JSONTransport attachment handling
|
||||
|
||||
## v4.1.0 2017-08-28
|
||||
|
||||
- Added new methods `createTestAccount` and `getTestMessageUrl` to use autogenerated email accounts from https://Ethereal.email
|
||||
|
||||
## v4.0.1 2017-04-13
|
||||
|
||||
- Fixed issue with LMTP and STARTTLS
|
||||
|
||||
## v4.0.0 2017-04-06
|
||||
|
||||
- License changed from EUPLv1.1 to MIT
|
||||
|
||||
## v3.1.8 2017-03-21
|
||||
|
||||
- Fixed invalid List-\* header generation
|
||||
|
||||
## v3.1.7 2017-03-14
|
||||
|
||||
- Emit an error if STARTTLS ends with connection being closed
|
||||
|
||||
## v3.1.6 2017-03-14
|
||||
|
||||
- Expose last server response for smtpConnection
|
||||
|
||||
## v3.1.5 2017-03-08
|
||||
|
||||
- Fixed SES transport, added missing `response` value
|
||||
|
||||
## v3.1.4 2017-02-26
|
||||
|
||||
- Fixed DKIM calculation for empty body
|
||||
- Ensure linebreak after message content. This fixes DKIM signatures for non-multipart messages where input did not end with a newline
|
||||
|
||||
## v3.1.3 2017-02-17
|
||||
|
||||
- Fixed missing `transport.verify()` methods for SES transport
|
||||
|
||||
## v3.1.2 2017-02-17
|
||||
|
||||
- Added missing error handlers for Sendmail, SES and Stream transports. If a messages contained an invalid URL as attachment then these transports threw an uncatched error
|
||||
|
||||
## v3.1.1 2017-02-13
|
||||
|
||||
- Fixed missing `transport.on('idle')` and `transport.isIdle()` methods for SES transports
|
||||
|
||||
## v3.1.0 2017-02-13
|
||||
|
||||
- Added built-in transport for AWS SES. [Docs](http://localhost:1313/transports/ses/)
|
||||
- Updated stream transport to allow building JSON strings. [Docs](http://localhost:1313/transports/stream/#json-transport)
|
||||
- Added new method _mail.resolveAll_ that fetches all attachments and such to be able to more easily build API-based transports
|
||||
|
||||
## v3.0.2 2017-02-04
|
||||
|
||||
- Fixed a bug with OAuth2 login where error callback was fired twice if getToken was not available.
|
||||
|
||||
## v3.0.1 2017-02-03
|
||||
|
||||
- Fixed a bug where Nodemailer threw an exception if `disableFileAccess` option was used
|
||||
- Added FLOSS [exception declaration](FLOSS_EXCEPTIONS.md)
|
||||
|
||||
## v3.0.0 2017-01-31
|
||||
|
||||
- Initial version of Nodemailer 3
|
||||
|
||||
This update brings a lot of breaking changes:
|
||||
|
||||
- License changed from MIT to **EUPL-1.1**. This was possible as the new version of Nodemailer is a major rewrite. The features I don't have ownership for, were removed or reimplemented. If there's still some snippets in the code that have vague ownership then notify <mailto:andris@kreata.ee> about the conflicting code and I'll fix it.
|
||||
- Requires **Node.js v6+**
|
||||
- All **templating is gone**. It was too confusing to use and to be really universal a huge list of different renderers would be required. Nodemailer is about email, not about parsing different template syntaxes
|
||||
- **No NTLM authentication**. It was too difficult to re-implement. If you still need it then it would be possible to introduce a pluggable SASL interface where you could load the NTLM module in your own code and pass it to Nodemailer. Currently this is not possible.
|
||||
- **OAuth2 authentication** is built in and has a different [configuration](https://nodemailer.com/smtp/oauth2/). You can use both user (3LO) and service (2LO) accounts to generate access tokens from Nodemailer. Additionally there's a new feature to authenticate differently for every message – useful if your application sends on behalf of different users instead of a single sender.
|
||||
- **Improved Calendaring**. Provide an ical file to Nodemailer to send out [calendar events](https://nodemailer.com/message/calendar-events/).
|
||||
|
||||
And also some non-breaking changes:
|
||||
|
||||
- All **dependencies were dropped**. There is exactly 0 dependencies needed to use Nodemailer. This brings the installation time of Nodemailer from NPM down to less than 2 seconds
|
||||
- **Delivery status notifications** added to Nodemailer
|
||||
- Improved and built-in **DKIM** signing of messages. Previously you needed an external module for this and it did quite a lousy job with larger messages
|
||||
- **Stream transport** to return a RFC822 formatted message as a stream. Useful if you want to use Nodemailer as a preprocessor and not for actual delivery.
|
||||
- **Sendmail** transport built-in, no need for external transport plugin
|
||||
|
||||
See [Nodemailer.com](https://nodemailer.com/) for full documentation
|
||||
|
||||
## 2.7.0 2016-12-08
|
||||
|
||||
- Bumped mailcomposer that generates encoded-words differently which might break some tests
|
||||
|
||||
## 2.6.0 2016-09-05
|
||||
|
||||
- Added new options disableFileAccess and disableUrlAccess
|
||||
- Fixed envelope handling where cc/bcc fields were ignored in the envelope object
|
||||
|
||||
## 2.4.2 2016-05-25
|
||||
|
||||
- Removed shrinkwrap file. Seemed to cause more trouble than help
|
||||
|
||||
## 2.4.1 2016-05-12
|
||||
|
||||
- Fixed outdated shrinkwrap file
|
||||
|
||||
## 2.4.0 2016-05-11
|
||||
|
||||
- Bumped mailcomposer module to allow using `false` as attachment filename (suppresses filename usage)
|
||||
- Added NTLM authentication support
|
||||
|
||||
## 2.3.2 2016-04-11
|
||||
|
||||
- Bumped smtp transport modules to get newest smtp-connection that fixes SMTPUTF8 support for internationalized email addresses
|
||||
|
||||
## 2.3.1 2016-04-08
|
||||
|
||||
- Bumped mailcomposer to have better support for message/822 attachments
|
||||
|
||||
## 2.3.0 2016-03-03
|
||||
|
||||
- Fixed a bug with attachment filename that contains mixed unicode and dashes
|
||||
- Added built-in support for proxies by providing a new SMTP option `proxy` that takes a proxy configuration url as its value
|
||||
- Added option `transport` to dynamically load transport plugins
|
||||
- Do not require globally installed grunt-cli
|
||||
|
||||
## 2.2.1 2016-02-20
|
||||
|
||||
- Fixed a bug in SMTP requireTLS option that was broken
|
||||
|
||||
## 2.2.0 2016-02-18
|
||||
|
||||
- Removed the need to use `clone` dependency
|
||||
- Added new method `verify` to check SMTP configuration
|
||||
- Direct transport uses STARTTLS by default, fallbacks to plaintext if STARTTLS fails
|
||||
- Added new message option `list` for setting List-\* headers
|
||||
- Add simple proxy support with `getSocket` method
|
||||
- Added new message option `textEncoding`. If `textEncoding` is not set then detect best encoding automatically
|
||||
- Added new message option `icalEvent` to embed iCalendar events. Example [here](examples/ical-event.js)
|
||||
- Added new attachment option `raw` to use prepared MIME contents instead of generating a new one. This might be useful when you want to handcraft some parts of the message yourself, for example if you want to inject a PGP encrypted message as the contents of a MIME node
|
||||
- Added new message option `raw` to use an existing MIME message instead of generating a new one
|
||||
|
||||
## 2.1.0 2016-02-01
|
||||
|
||||
Republishing 2.1.0-rc.1 as stable. To recap, here's the notable changes between v2.0 and v2.1:
|
||||
|
||||
- Implemented templating support. You can either use a simple built-in renderer or some external advanced renderer, eg. [node-email-templates](https://github.com/niftylettuce/node-email-templates). Templating [docs](http://nodemailer.com/2-0-0-beta/templating/).
|
||||
- Updated smtp-pool to emit 'idle' events in order to handle message queue more effectively
|
||||
- Updated custom header handling, works everywhere the same now, no differences between adding custom headers to the message or to an attachment
|
||||
|
||||
## 2.1.0-rc.1 2016-01-25
|
||||
|
||||
Sneaked in some new features even though it is already rc
|
||||
|
||||
- If a SMTP pool is closed while there are still messages in a queue, the message callbacks are invoked with an error
|
||||
- In case of SMTP pool the transporter emits 'idle' when there is a free connection slot available
|
||||
- Added method `isIdle()` that checks if a pool has still some free connection slots available
|
||||
|
||||
## 2.1.0-rc.0 2016-01-20
|
||||
|
||||
- Bumped dependency versions
|
||||
|
||||
## 2.1.0-beta.3 2016-01-20
|
||||
|
||||
- Added support for node-email-templates templating in addition to the built-in renderer
|
||||
|
||||
## 2.1.0-beta.2 2016-01-20
|
||||
|
||||
- Implemented simple templating feature
|
||||
|
||||
## 2.1.0-beta.1 2016-01-20
|
||||
|
||||
- Allow using prepared header values that are not folded or encoded by Nodemailer
|
||||
|
||||
## 2.1.0-beta.0 2016-01-20
|
||||
|
||||
- Use the same header custom structure for message root, attachments and alternatives
|
||||
- Ensure that Message-Id exists when accessing message
|
||||
- Allow using array values for custom headers (inserts every value in its own row)
|
||||
|
||||
## 2.0.0 2016-01-11
|
||||
|
||||
- Released rc.2 as stable
|
||||
|
||||
## 2.0.0-rc.2 2016-01-04
|
||||
|
||||
- Locked dependencies
|
||||
|
||||
## 2.0.0-beta.2 2016-01-04
|
||||
|
||||
- Updated documentation to reflect changes with SMTP handling
|
||||
- Use beta versions for smtp/pool/direct transports
|
||||
- Updated logging
|
||||
|
||||
## 2.0.0-beta.1 2016-01-03
|
||||
|
||||
- Use bunyan compatible logger instead of the emit('log') style
|
||||
- Outsourced some reusable methods to nodemailer-shared
|
||||
- Support setting direct/smtp/pool with the default configuration
|
||||
|
||||
## 2.0.0-beta.0 2015-12-31
|
||||
|
||||
- Stream errors are not silently swallowed
|
||||
- Do not use format=flowed
|
||||
- Use nodemailer-fetch to fetch URL streams
|
||||
- jshint replaced by eslint
|
||||
|
||||
## v1.11.0 2015-12-28
|
||||
|
||||
Allow connection url based SMTP configurations
|
||||
|
||||
## v1.10.0 2015-11-13
|
||||
|
||||
Added `defaults` argument for `createTransport` to predefine commonn values (eg. `from` address)
|
||||
|
||||
## v1.9.0 2015-11-09
|
||||
|
||||
Returns a Promise for `sendMail` if callback is not defined
|
||||
|
||||
## v1.8.0 2015-10-08
|
||||
|
||||
Added priority option (high, normal, low) for setting Importance header
|
||||
|
||||
## v1.7.0 2015-10-06
|
||||
|
||||
Replaced hyperquest with needle. Fixes issues with compressed data and redirects
|
||||
|
||||
## v1.6.0 2015-10-05
|
||||
|
||||
Maintenance release. Bumped dependencies to get support for unicode filenames for QQ webmail and to support emoji in filenames
|
||||
|
||||
## v1.5.0 2015-09-24
|
||||
|
||||
Use mailcomposer instead of built in solution to generate message sources. Bumped libmime gives better quoted-printable handling.
|
||||
|
||||
## v1.4.0 2015-06-27
|
||||
|
||||
Added new message option `watchHtml` to specify Apple Watch specific HTML part of the message. See [this post](https://litmus.com/blog/how-to-send-hidden-version-email-apple-watch) for details
|
||||
|
||||
## v1.3.4 2015-04-25
|
||||
|
||||
Maintenance release, bumped buildmail version to get fixed format=flowed handling
|
||||
|
||||
## v1.3.3 2015-04-25
|
||||
|
||||
Maintenance release, bumped dependencies
|
||||
|
||||
## v1.3.2 2015-03-09
|
||||
|
||||
Maintenance release, upgraded dependencies. Replaced simplesmtp based tests with smtp-server based ones.
|
||||
|
||||
## v1.3.0 2014-09-12
|
||||
|
||||
Maintenance release, upgrades buildmail and libmime. Allows using functions as transform plugins and fixes issue with unicode filenames in Gmail.
|
||||
|
||||
## v1.2.2 2014-09-05
|
||||
|
||||
Proper handling of data uris as attachments. Attachment `path` property can also be defined as a data uri, not just regular url or file path.
|
||||
|
||||
## v1.2.1 2014-08-21
|
||||
|
||||
Bumped libmime and mailbuild versions to properly handle filenames with spaces (short ascii only filenames with spaces were left unquoted).
|
||||
|
||||
## v1.2.0 2014-08-18
|
||||
|
||||
Allow using encoded strings as attachments. Added new property `encoding` which defines the encoding used for a `content` string. If encoding is set, the content value is converted to a Buffer value using the defined encoding before usage. Useful for including binary attachemnts in JSON formatted email objects.
|
||||
|
||||
## v1.1.2 2014-08-18
|
||||
|
||||
Return deprecatin error for v0.x style configuration
|
||||
|
||||
## v1.1.1 2014-07-30
|
||||
|
||||
Bumped nodemailer-direct-transport dependency. Updated version includes a bugfix for Stream nodes handling. Important only if use direct-transport with Streams (not file paths or urls) as attachment content.
|
||||
|
||||
## v1.1.0 2014-07-29
|
||||
|
||||
Added new method `resolveContent()` to get the html/text/attachment content as a String or Buffer.
|
||||
|
||||
## v1.0.4 2014-07-23
|
||||
|
||||
Bugfix release. HTML node was instered twice if the message consisted of a HTML content (but no text content) + at least one attachment with CID + at least one attachment without CID. In this case the HTML node was inserted both to the root level multipart/mixed section and to the multipart/related sub section
|
||||
|
||||
## v1.0.3 2014-07-16
|
||||
|
||||
Fixed a bug where Nodemailer crashed if the message content type was multipart/related
|
||||
|
||||
## v1.0.2 2014-07-16
|
||||
|
||||
Upgraded nodemailer-smtp-transport to 0.1.11\. The docs state that for SSL you should use 'secure' option but the underlying smtp-connection module used 'secureConnection' for this purpose. Fixed smpt-connection to match the docs.
|
||||
|
||||
## v1.0.1 2014-07-15
|
||||
|
||||
Implemented missing #close method that is passed to the underlying transport object. Required by the smtp pool.
|
||||
|
||||
## v1.0.0 2014-07-15
|
||||
|
||||
Total rewrite. See migration guide here: <http://www.andrisreinman.com/nodemailer-v1-0/#migrationguide>
|
||||
|
||||
## v0.7.1 2014-07-09
|
||||
|
||||
- Upgraded aws-sdk to 2.0.5
|
||||
|
||||
## v0.7.0 2014-06-17
|
||||
|
||||
- Bumped version to v0.7.0
|
||||
- Fix AWS-SES usage [5b6bc144]
|
||||
- Replace current SES with new SES using AWS-SDK (Elanorr) [c79d797a]
|
||||
- Updated README.md about Node Email Templates (niftylettuce) [e52bef81]
|
||||
|
||||
## v0.6.5 2014-05-15
|
||||
|
||||
- Bumped version to v0.6.5
|
||||
- Use tildes instead of carets for dependency listing [5296ce41]
|
||||
- Allow clients to set a custom identityString (venables) [5373287d]
|
||||
- bugfix (adding "-i" to sendmail command line for each new mail) by copying this.args (vrodic) [05a8a9a3]
|
||||
- update copyright (gdi2290) [3a6cba3a]
|
||||
|
||||
## v0.6.4 2014-05-13
|
||||
|
||||
- Bumped version to v0.6.4
|
||||
- added npmignore, bumped dependencies [21bddcd9]
|
||||
- Add AOL to well-known services (msouce) [da7dd3b7]
|
||||
|
||||
## v0.6.3 2014-04-16
|
||||
|
||||
- Bumped version to v0.6.3
|
||||
- Upgraded simplesmtp dependency [dd367f59]
|
||||
|
||||
## v0.6.2 2014-04-09
|
||||
|
||||
- Bumped version to v0.6.2
|
||||
- Added error option to Stub transport [c423acad]
|
||||
- Use SVG npm badge (t3chnoboy) [677117b7]
|
||||
- add SendCloud to well known services (haio) [43c358e0]
|
||||
- High-res build-passing and NPM module badges (sahat) [9fdc37cd]
|
||||
|
||||
## v0.6.1 2014-01-26
|
||||
|
||||
- Bumped version to v0.6.1
|
||||
- Do not throw on multiple errors from sendmail command [c6e2cd12]
|
||||
- Do not require callback for pickup, fixes #238 [93eb3214]
|
||||
- Added AWSSecurityToken information to README, fixes #235 [58e921d1]
|
||||
- Added Nodemailer logo [06b7d1a8]
|
||||
|
||||
## v0.6.0 2013-12-30
|
||||
|
||||
- Bumped version to v0.6.0
|
||||
- Allow defining custom transport methods [ec5b48ce]
|
||||
- Return messageId with responseObject for all built in transport methods [74445cec]
|
||||
- Bumped dependency versions for mailcomposer and readable-stream [9a034c34]
|
||||
- Changed pickup argument name to 'directory' [01c3ea53]
|
||||
- Added support for IIS pickup directory with PICKUP transport (philipproplesch) [36940b59..360a2878]
|
||||
- Applied common styles [9e93a409]
|
||||
- Updated readme [c78075e7]
|
||||
|
||||
## v0.5.15 2013-12-13
|
||||
|
||||
- bumped version to v0.5.15
|
||||
- Updated README, added global options info for setting uo transports [554bb0e5]
|
||||
- Resolve public hostname, if resolveHostname property for a transport object is set to `true` [9023a6e1..4c66b819]
|
||||
|
||||
## v0.5.14 2013-12-05
|
||||
|
||||
- bumped version to v0.5.14
|
||||
- Expose status for direct messages [f0312df6]
|
||||
- Allow to skip the X-Mailer header if xMailer value is set to 'false' [f2c20a68]
|
||||
|
||||
## v0.5.13 2013-12-03
|
||||
|
||||
- bumped version to v0.5.13
|
||||
- Use the name property from the transport object to use for the domain part of message-id values (1598eee9)
|
||||
|
||||
## v0.5.12 2013-12-02
|
||||
|
||||
- bumped version to v0.5.12
|
||||
- Expose transport method and transport module version if available [a495106e]
|
||||
- Added 'he' module instead of using custom html entity decoding [c197d102]
|
||||
- Added xMailer property for transport configuration object to override X-Mailer value [e8733a61]
|
||||
- Updated README, added description for 'mail' method [e1f5f3a6]
|
||||
|
||||
## v0.5.11 2013-11-28
|
||||
|
||||
- bumped version to v0.5.11
|
||||
- Updated mailcomposer version. Replaces ent with he [6a45b790e]
|
||||
|
||||
## v0.5.10 2013-11-26
|
||||
|
||||
- bumped version to v0.5.10
|
||||
- added shorthand function mail() for direct transport type [88129bd7]
|
||||
- minor tweaks and typo fixes [f797409e..ceac0ca4]
|
||||
|
||||
## v0.5.9 2013-11-25
|
||||
|
||||
- bumped version to v0.5.9
|
||||
- Update for 'direct' handling [77b84e2f]
|
||||
- do not require callback to be provided for 'direct' type [ec51c79f]
|
||||
|
||||
## v0.5.8 2013-11-22
|
||||
|
||||
- bumped version to v0.5.8
|
||||
- Added support for 'direct' transport [826f226d..0dbbcbbc]
|
||||
|
||||
## v0.5.7 2013-11-18
|
||||
|
||||
- bumped version to v0.5.7
|
||||
- Replace \r\n by \n in Sendmail transport (rolftimmermans) [fed2089e..616ec90c] A lot of sendmail implementations choke on \r\n newlines and require \n This commit addresses this by transforming all \r\n sequences passed to the sendmail command with \n
|
||||
|
||||
## v0.5.6 2013-11-15
|
||||
|
||||
- bumped version to v0.5.6
|
||||
- Upgraded mailcomposer dependency to 0.2.4 [e5ff9c40]
|
||||
- Removed noCR option [e810d1b8]
|
||||
- Update wellknown.js, added FastMail (k-j-kleist) [cf930f6d]
|
||||
|
||||
## v0.5.5 2013-10-30
|
||||
|
||||
- bumped version to v0.5.5
|
||||
- Updated mailcomposer dependnecy version to 0.2.3
|
||||
- Remove legacy code - node v0.4 is not supported anymore anyway
|
||||
- Use hostname (autodetected or from the options.name property) for Message-Id instead of "Nodemailer" (helps a bit when messages are identified as spam)
|
||||
- Added maxMessages info to README
|
||||
|
||||
## v0.5.4 2013-10-29
|
||||
|
||||
- bumped version to v0.5.4
|
||||
- added "use strict" statements
|
||||
- Added DSN info to README
|
||||
- add support for QQ enterprise email (coderhaoxin)
|
||||
- Add a Bitdeli Badge to README
|
||||
- DSN options Passthrought into simplesmtp. (irvinzz)
|
||||
|
||||
## v0.5.3 2013-10-03
|
||||
|
||||
- bumped version v0.5.3
|
||||
- Using a stub transport to prevent sendmail from being called during a test. (jsdevel)
|
||||
- closes #78: sendmail transport does not work correctly on Unix machines. (jsdevel)
|
||||
- Updated PaaS Support list to include Modulus. (fiveisprime)
|
||||
- Translate self closing break tags to newline (kosmasgiannis)
|
||||
- fix typos (aeosynth)
|
||||
|
||||
## v0.5.2 2013-07-25
|
||||
|
||||
- bumped version v0.5.2
|
||||
- Merge pull request #177 from MrSwitch/master Fixing Amazon SES, fatal error caused by bad connection
|
||||
76
backend/node_modules/imapflow/node_modules/nodemailer/CODE_OF_CONDUCT.md
generated
vendored
Normal file
76
backend/node_modules/imapflow/node_modules/nodemailer/CODE_OF_CONDUCT.md
generated
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
- Using welcoming and inclusive language
|
||||
- Being respectful of differing viewpoints and experiences
|
||||
- Gracefully accepting constructive criticism
|
||||
- Focusing on what is best for the community
|
||||
- Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
- The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
- Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at info@nodemailer.com. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
16
backend/node_modules/imapflow/node_modules/nodemailer/LICENSE
generated
vendored
Normal file
16
backend/node_modules/imapflow/node_modules/nodemailer/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
Copyright (c) 2011-2023 Andris Reinman
|
||||
|
||||
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 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.
|
||||
86
backend/node_modules/imapflow/node_modules/nodemailer/README.md
generated
vendored
Normal file
86
backend/node_modules/imapflow/node_modules/nodemailer/README.md
generated
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
# Nodemailer
|
||||
|
||||
[](https://nodemailer.com/about/)
|
||||
|
||||
Send emails from Node.js – easy as cake! 🍰✉️
|
||||
|
||||
[](https://nodemailer.com/about/)
|
||||
|
||||
See [nodemailer.com](https://nodemailer.com/) for documentation and terms.
|
||||
|
||||
> [!TIP]
|
||||
> Check out **[EmailEngine](https://emailengine.app/?utm_source=github-nodemailer&utm_campaign=nodemailer&utm_medium=readme-link)** – a self-hosted email gateway that allows making **REST requests against IMAP and SMTP servers**. EmailEngine also sends webhooks whenever something changes on the registered accounts.\
|
||||
> \
|
||||
> Using the email accounts registered with EmailEngine, you can receive and [send emails](https://emailengine.app/sending-emails?utm_source=github-nodemailer&utm_campaign=nodemailer&utm_medium=readme-link). EmailEngine supports OAuth2, delayed sends, opens and clicks tracking, bounce detection, etc. All on top of regular email accounts without an external MTA service.
|
||||
|
||||
## Having an issue?
|
||||
|
||||
#### First review the docs
|
||||
|
||||
Documentation for Nodemailer can be found at [nodemailer.com](https://nodemailer.com/about/).
|
||||
|
||||
#### Nodemailer throws a SyntaxError for "..."
|
||||
|
||||
You are using an older Node.js version than v6.0. Upgrade Node.js to get support for the spread operator. Nodemailer supports all Node.js versions starting from Node.js@v6.0.0.
|
||||
|
||||
#### I'm having issues with Gmail
|
||||
|
||||
Gmail either works well, or it does not work at all. It is probably easier to switch to an alternative service instead of fixing issues with Gmail. If Gmail does not work for you, then don't use it. Read more about it [here](https://nodemailer.com/usage/using-gmail/).
|
||||
|
||||
#### I get ETIMEDOUT errors
|
||||
|
||||
Check your firewall settings. Timeout usually occurs when you try to open a connection to a firewalled port either on the server or on your machine. Some ISPs also block email ports to prevent spamming.
|
||||
|
||||
#### Nodemailer works on one machine but not in another
|
||||
|
||||
It's either a firewall issue, or your SMTP server blocks authentication attempts from some servers.
|
||||
|
||||
#### I get TLS errors
|
||||
|
||||
- If you are running the code on your machine, check your antivirus settings. Antiviruses often mess around with email ports usage. Node.js might not recognize the MITM cert your antivirus is using.
|
||||
- Latest Node versions allow only TLS versions 1.2 and higher. Some servers might still use TLS 1.1 or lower. Check Node.js docs on how to get correct TLS support for your app. You can change this with [tls.minVersion](https://nodejs.org/dist/latest-v16.x/docs/api/tls.html#tls_tls_createsecurecontext_options) option
|
||||
- You might have the wrong value for the `secure` option. This should be set to `true` only for port 465. For every other port, it should be `false`. Setting it to `false` does not mean that Nodemailer would not use TLS. Nodemailer would still try to upgrade the connection to use TLS if the server supports it.
|
||||
- Older Node versions do not fully support the certificate chain of the newest Let's Encrypt certificates. Either set [tls.rejectUnauthorized](https://nodejs.org/dist/latest-v16.x/docs/api/tls.html#tlsconnectoptions-callback) to `false` to skip chain verification or upgrade your Node version
|
||||
|
||||
```js
|
||||
let configOptions = {
|
||||
host: 'smtp.example.com',
|
||||
port: 587,
|
||||
tls: {
|
||||
rejectUnauthorized: true,
|
||||
minVersion: 'TLSv1.2'
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
#### I have issues with DNS / hosts file
|
||||
|
||||
Node.js uses [c-ares](https://nodejs.org/en/docs/meta/topics/dependencies/#c-ares) to resolve domain names, not the DNS library provided by the system, so if you have some custom DNS routing set up, it might be ignored. Nodemailer runs [dns.resolve4()](https://nodejs.org/dist/latest-v16.x/docs/api/dns.html#dnsresolve4hostname-options-callback) and [dns.resolve6()](https://nodejs.org/dist/latest-v16.x/docs/api/dns.html#dnsresolve6hostname-options-callback) to resolve hostname into an IP address. If both calls fail, then Nodemailer will fall back to [dns.lookup()](https://nodejs.org/dist/latest-v16.x/docs/api/dns.html#dnslookuphostname-options-callback). If this does not work for you, you can hard code the IP address into the configuration like shown below. In that case, Nodemailer would not perform any DNS lookups.
|
||||
|
||||
```js
|
||||
let configOptions = {
|
||||
host: '1.2.3.4',
|
||||
port: 465,
|
||||
secure: true,
|
||||
tls: {
|
||||
// must provide server name, otherwise TLS certificate check will fail
|
||||
servername: 'example.com'
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
#### I have an issue with TypeScript types
|
||||
|
||||
Nodemailer has official support for Node.js only. For anything related to TypeScript, you need to directly contact the authors of the [type definitions](https://www.npmjs.com/package/@types/nodemailer).
|
||||
|
||||
#### I have a different problem
|
||||
|
||||
If you are having issues with Nodemailer, then the best way to find help would be [Stack Overflow](https://stackoverflow.com/search?q=nodemailer) or revisit the [docs](https://nodemailer.com/about/).
|
||||
|
||||
### License
|
||||
|
||||
Nodemailer is licensed under the **MIT No Attribution license**
|
||||
|
||||
---
|
||||
|
||||
The Nodemailer logo was designed by [Sven Kristjansen](https://www.behance.net/kristjansen).
|
||||
22
backend/node_modules/imapflow/node_modules/nodemailer/SECURITY.txt
generated
vendored
Normal file
22
backend/node_modules/imapflow/node_modules/nodemailer/SECURITY.txt
generated
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
-----BEGIN PGP SIGNED MESSAGE-----
|
||||
Hash: SHA256
|
||||
|
||||
Contact: mailto:andris@reinman.eu
|
||||
Encryption: https://keys.openpgp.org/vks/v1/by-fingerprint/5D952A46E1D8C931F6364E01DC6C83F4D584D364
|
||||
Preferred-Languages: en, et
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQIzBAEBCAAdFiEEXZUqRuHYyTH2Nk4B3GyD9NWE02QFAmFDnUgACgkQ3GyD9NWE
|
||||
02RqUA/+MM3afmRYq874C7wp+uN6dTMCvUX5g5zqBZ2yKpFr46L+PYvM7o8TMm5h
|
||||
hmLT2I1zZmi+xezOL3zHFizaw0tKkZIz9cWl3Jrgs0FLp0zOsSz1xucp9Q2tYM/Q
|
||||
vbiP6ys0gbim4tkDGRmZOEiO23s0BuRnmHt7vZg210O+D105Yd8/Ohzbj6PSLBO5
|
||||
W1tA7Xw5t0FQ14NNH5+MKyDIKoCX12n0FmrC6qLTXeojf291UgKhCUPda3LIGTmx
|
||||
mTXz0y68149Mw+JikRCYP8HfGRY9eA4XZrYXF7Bl2T9OJpKD3JAH+69P3xBw19Gn
|
||||
Csaw3twu8P1bxoVGjY4KRrBOp68W8TwZYjWVWbqY6oV8hb/JfrMxa+kaSxRuloFs
|
||||
oL6+phrDSPTWdOj2LlEDBJbPOMeDFzIlsBBcJ/JHCEHTvlHl7LoWr3YuWce9PUwl
|
||||
4r3JUovvaeuJxLgC0vu3WCB3Jeocsl3SreqNkrVc1IjvkSomn3YGm5nCNAd/2F0V
|
||||
exCGRk/8wbkSjAY38GwQ8K/VuFsefWN3L9sVwIMAMu88KFCAN+GzVFiwvyIXehF5
|
||||
eogP9mIXzdQ5YReQjUjApOzGz54XnDyv9RJ3sdvMHosLP+IOg+0q5t9agWv6aqSR
|
||||
2HzCpiQnH/gmM5NS0AU4Koq/L7IBeLu1B8+61/+BiHgZJJmPdgU=
|
||||
=BUZr
|
||||
-----END PGP SIGNATURE-----
|
||||
88
backend/node_modules/imapflow/node_modules/nodemailer/eslint.config.js
generated
vendored
Normal file
88
backend/node_modules/imapflow/node_modules/nodemailer/eslint.config.js
generated
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
'use strict';
|
||||
|
||||
const globals = require('globals');
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
ignores: ['node_modules/**', 'coverage/**', 'dist/**', 'build/**', '.nyc_output/**']
|
||||
},
|
||||
{
|
||||
files: ['**/*.js'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2017,
|
||||
sourceType: 'script',
|
||||
globals: Object.assign({}, globals.node, globals.es2017, {
|
||||
it: true,
|
||||
describe: true,
|
||||
beforeEach: true,
|
||||
afterEach: true
|
||||
})
|
||||
},
|
||||
rules: {
|
||||
// Error detection
|
||||
'for-direction': 'error',
|
||||
'no-await-in-loop': 'error',
|
||||
'no-div-regex': 'error',
|
||||
eqeqeq: 'error',
|
||||
'dot-notation': 'error',
|
||||
curly: 'error',
|
||||
'no-fallthrough': 'error',
|
||||
'no-unused-expressions': [
|
||||
'error',
|
||||
{
|
||||
allowShortCircuit: true
|
||||
}
|
||||
],
|
||||
'no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
varsIgnorePattern: '^_',
|
||||
argsIgnorePattern: '^_',
|
||||
caughtErrorsIgnorePattern: '^_'
|
||||
}
|
||||
],
|
||||
'handle-callback-err': 'error',
|
||||
'no-new': 'error',
|
||||
'new-cap': 'error',
|
||||
'no-eval': 'error',
|
||||
'no-invalid-this': 'error',
|
||||
radix: ['error', 'always'],
|
||||
'no-use-before-define': ['error', 'nofunc'],
|
||||
'callback-return': ['error', ['callback', 'cb', 'done']],
|
||||
'no-regex-spaces': 'error',
|
||||
'no-empty': 'error',
|
||||
'no-duplicate-case': 'error',
|
||||
'no-empty-character-class': 'error',
|
||||
'no-redeclare': 'off', // Disabled per project preference
|
||||
'block-scoped-var': 'error',
|
||||
'no-sequences': 'error',
|
||||
'no-throw-literal': 'error',
|
||||
'no-useless-call': 'error',
|
||||
'no-useless-concat': 'error',
|
||||
'no-void': 'error',
|
||||
yoda: 'error',
|
||||
'no-undef': 'error',
|
||||
'global-require': 'error',
|
||||
'no-var': 'error',
|
||||
'no-bitwise': 'error',
|
||||
'no-lonely-if': 'error',
|
||||
'no-mixed-spaces-and-tabs': 'error',
|
||||
'arrow-body-style': ['error', 'as-needed'],
|
||||
'arrow-parens': ['error', 'as-needed'],
|
||||
'prefer-arrow-callback': 'error',
|
||||
'object-shorthand': 'error',
|
||||
'prefer-spread': 'error',
|
||||
'no-prototype-builtins': 'off', // Disabled per project preference
|
||||
strict: ['error', 'global'],
|
||||
|
||||
// Disable all formatting rules (handled by Prettier)
|
||||
indent: 'off',
|
||||
quotes: 'off',
|
||||
'linebreak-style': 'off',
|
||||
semi: 'off',
|
||||
'quote-props': 'off',
|
||||
'comma-dangle': 'off',
|
||||
'comma-style': 'off'
|
||||
}
|
||||
}
|
||||
];
|
||||
383
backend/node_modules/imapflow/node_modules/nodemailer/lib/addressparser/index.js
generated
vendored
Normal file
383
backend/node_modules/imapflow/node_modules/nodemailer/lib/addressparser/index.js
generated
vendored
Normal file
@@ -0,0 +1,383 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Converts tokens for a single address into an address object
|
||||
*
|
||||
* @param {Array} tokens Tokens object
|
||||
* @param {Number} depth Current recursion depth for nested group protection
|
||||
* @return {Object} Address object
|
||||
*/
|
||||
function _handleAddress(tokens, depth) {
|
||||
let isGroup = false;
|
||||
let state = 'text';
|
||||
let address;
|
||||
let addresses = [];
|
||||
let data = {
|
||||
address: [],
|
||||
comment: [],
|
||||
group: [],
|
||||
text: [],
|
||||
textWasQuoted: [] // Track which text tokens came from inside quotes
|
||||
};
|
||||
let i;
|
||||
let len;
|
||||
let insideQuotes = false; // Track if we're currently inside a quoted string
|
||||
|
||||
// Filter out <addresses>, (comments) and regular text
|
||||
for (i = 0, len = tokens.length; i < len; i++) {
|
||||
let token = tokens[i];
|
||||
let prevToken = i ? tokens[i - 1] : null;
|
||||
if (token.type === 'operator') {
|
||||
switch (token.value) {
|
||||
case '<':
|
||||
state = 'address';
|
||||
insideQuotes = false;
|
||||
break;
|
||||
case '(':
|
||||
state = 'comment';
|
||||
insideQuotes = false;
|
||||
break;
|
||||
case ':':
|
||||
state = 'group';
|
||||
isGroup = true;
|
||||
insideQuotes = false;
|
||||
break;
|
||||
case '"':
|
||||
// Track quote state for text tokens
|
||||
insideQuotes = !insideQuotes;
|
||||
state = 'text';
|
||||
break;
|
||||
default:
|
||||
state = 'text';
|
||||
insideQuotes = false;
|
||||
break;
|
||||
}
|
||||
} else if (token.value) {
|
||||
if (state === 'address') {
|
||||
// handle use case where unquoted name includes a "<"
|
||||
// Apple Mail truncates everything between an unexpected < and an address
|
||||
// and so will we
|
||||
token.value = token.value.replace(/^[^<]*<\s*/, '');
|
||||
}
|
||||
|
||||
if (prevToken && prevToken.noBreak && data[state].length) {
|
||||
// join values
|
||||
data[state][data[state].length - 1] += token.value;
|
||||
if (state === 'text' && insideQuotes) {
|
||||
data.textWasQuoted[data.textWasQuoted.length - 1] = true;
|
||||
}
|
||||
} else {
|
||||
data[state].push(token.value);
|
||||
if (state === 'text') {
|
||||
data.textWasQuoted.push(insideQuotes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there is no text but a comment, replace the two
|
||||
if (!data.text.length && data.comment.length) {
|
||||
data.text = data.comment;
|
||||
data.comment = [];
|
||||
}
|
||||
|
||||
if (isGroup) {
|
||||
// http://tools.ietf.org/html/rfc2822#appendix-A.1.3
|
||||
data.text = data.text.join(' ');
|
||||
|
||||
// Parse group members, but flatten any nested groups (RFC 5322 doesn't allow nesting)
|
||||
let groupMembers = [];
|
||||
if (data.group.length) {
|
||||
let parsedGroup = addressparser(data.group.join(','), { _depth: depth + 1 });
|
||||
// Flatten: if any member is itself a group, extract its members into the sequence
|
||||
parsedGroup.forEach(member => {
|
||||
if (member.group) {
|
||||
// Nested group detected - flatten it by adding its members directly
|
||||
groupMembers = groupMembers.concat(member.group);
|
||||
} else {
|
||||
groupMembers.push(member);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addresses.push({
|
||||
name: data.text || (address && address.name),
|
||||
group: groupMembers
|
||||
});
|
||||
} else {
|
||||
// If no address was found, try to detect one from regular text
|
||||
if (!data.address.length && data.text.length) {
|
||||
for (i = data.text.length - 1; i >= 0; i--) {
|
||||
// Security fix: Do not extract email addresses from quoted strings
|
||||
// RFC 5321 allows @ inside quoted local-parts like "user@domain"@example.com
|
||||
// Extracting emails from quoted text leads to misrouting vulnerabilities
|
||||
if (!data.textWasQuoted[i] && data.text[i].match(/^[^@\s]+@[^@\s]+$/)) {
|
||||
data.address = data.text.splice(i, 1);
|
||||
data.textWasQuoted.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let _regexHandler = function (address) {
|
||||
if (!data.address.length) {
|
||||
data.address = [address.trim()];
|
||||
return ' ';
|
||||
} else {
|
||||
return address;
|
||||
}
|
||||
};
|
||||
|
||||
// still no address
|
||||
if (!data.address.length) {
|
||||
for (i = data.text.length - 1; i >= 0; i--) {
|
||||
// Security fix: Do not extract email addresses from quoted strings
|
||||
if (!data.textWasQuoted[i]) {
|
||||
// fixed the regex to parse email address correctly when email address has more than one @
|
||||
data.text[i] = data.text[i].replace(/\s*\b[^@\s]+@[^\s]+\b\s*/, _regexHandler).trim();
|
||||
if (data.address.length) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there's still is no text but a comment exixts, replace the two
|
||||
if (!data.text.length && data.comment.length) {
|
||||
data.text = data.comment;
|
||||
data.comment = [];
|
||||
}
|
||||
|
||||
// Keep only the first address occurence, push others to regular text
|
||||
if (data.address.length > 1) {
|
||||
data.text = data.text.concat(data.address.splice(1));
|
||||
}
|
||||
|
||||
// Join values with spaces
|
||||
data.text = data.text.join(' ');
|
||||
data.address = data.address.join(' ');
|
||||
|
||||
if (!data.address && isGroup) {
|
||||
return [];
|
||||
} else {
|
||||
address = {
|
||||
address: data.address || data.text || '',
|
||||
name: data.text || data.address || ''
|
||||
};
|
||||
|
||||
if (address.address === address.name) {
|
||||
if ((address.address || '').match(/@/)) {
|
||||
address.name = '';
|
||||
} else {
|
||||
address.address = '';
|
||||
}
|
||||
}
|
||||
|
||||
addresses.push(address);
|
||||
}
|
||||
}
|
||||
|
||||
return addresses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Tokenizer object for tokenizing address field strings
|
||||
*
|
||||
* @constructor
|
||||
* @param {String} str Address field string
|
||||
*/
|
||||
class Tokenizer {
|
||||
constructor(str) {
|
||||
this.str = (str || '').toString();
|
||||
this.operatorCurrent = '';
|
||||
this.operatorExpecting = '';
|
||||
this.node = null;
|
||||
this.escaped = false;
|
||||
|
||||
this.list = [];
|
||||
/**
|
||||
* Operator tokens and which tokens are expected to end the sequence
|
||||
*/
|
||||
this.operators = {
|
||||
'"': '"',
|
||||
'(': ')',
|
||||
'<': '>',
|
||||
',': '',
|
||||
':': ';',
|
||||
// Semicolons are not a legal delimiter per the RFC2822 grammar other
|
||||
// than for terminating a group, but they are also not valid for any
|
||||
// other use in this context. Given that some mail clients have
|
||||
// historically allowed the semicolon as a delimiter equivalent to the
|
||||
// comma in their UI, it makes sense to treat them the same as a comma
|
||||
// when used outside of a group.
|
||||
';': ''
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Tokenizes the original input string
|
||||
*
|
||||
* @return {Array} An array of operator|text tokens
|
||||
*/
|
||||
tokenize() {
|
||||
let list = [];
|
||||
|
||||
for (let i = 0, len = this.str.length; i < len; i++) {
|
||||
let chr = this.str.charAt(i);
|
||||
let nextChr = i < len - 1 ? this.str.charAt(i + 1) : null;
|
||||
this.checkChar(chr, nextChr);
|
||||
}
|
||||
|
||||
this.list.forEach(node => {
|
||||
node.value = (node.value || '').toString().trim();
|
||||
if (node.value) {
|
||||
list.push(node);
|
||||
}
|
||||
});
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a character is an operator or text and acts accordingly
|
||||
*
|
||||
* @param {String} chr Character from the address field
|
||||
*/
|
||||
checkChar(chr, nextChr) {
|
||||
if (this.escaped) {
|
||||
// ignore next condition blocks
|
||||
} else if (chr === this.operatorExpecting) {
|
||||
this.node = {
|
||||
type: 'operator',
|
||||
value: chr
|
||||
};
|
||||
|
||||
if (nextChr && ![' ', '\t', '\r', '\n', ',', ';'].includes(nextChr)) {
|
||||
this.node.noBreak = true;
|
||||
}
|
||||
|
||||
this.list.push(this.node);
|
||||
this.node = null;
|
||||
this.operatorExpecting = '';
|
||||
this.escaped = false;
|
||||
|
||||
return;
|
||||
} else if (!this.operatorExpecting && chr in this.operators) {
|
||||
this.node = {
|
||||
type: 'operator',
|
||||
value: chr
|
||||
};
|
||||
this.list.push(this.node);
|
||||
this.node = null;
|
||||
this.operatorExpecting = this.operators[chr];
|
||||
this.escaped = false;
|
||||
return;
|
||||
} else if (['"', "'"].includes(this.operatorExpecting) && chr === '\\') {
|
||||
this.escaped = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.node) {
|
||||
this.node = {
|
||||
type: 'text',
|
||||
value: ''
|
||||
};
|
||||
this.list.push(this.node);
|
||||
}
|
||||
|
||||
if (chr === '\n') {
|
||||
// Convert newlines to spaces. Carriage return is ignored as \r and \n usually
|
||||
// go together anyway and there already is a WS for \n. Lone \r means something is fishy.
|
||||
chr = ' ';
|
||||
}
|
||||
|
||||
if (chr.charCodeAt(0) >= 0x21 || [' ', '\t'].includes(chr)) {
|
||||
// skip command bytes
|
||||
this.node.value += chr;
|
||||
}
|
||||
|
||||
this.escaped = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum recursion depth for parsing nested groups.
|
||||
* RFC 5322 doesn't allow nested groups, so this is a safeguard against
|
||||
* malicious input that could cause stack overflow.
|
||||
*/
|
||||
const MAX_NESTED_GROUP_DEPTH = 50;
|
||||
|
||||
/**
|
||||
* Parses structured e-mail addresses from an address field
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* 'Name <address@domain>'
|
||||
*
|
||||
* will be converted to
|
||||
*
|
||||
* [{name: 'Name', address: 'address@domain'}]
|
||||
*
|
||||
* @param {String} str Address field
|
||||
* @param {Object} options Optional options object
|
||||
* @param {Number} options._depth Internal recursion depth counter (do not set manually)
|
||||
* @return {Array} An array of address objects
|
||||
*/
|
||||
function addressparser(str, options) {
|
||||
options = options || {};
|
||||
let depth = options._depth || 0;
|
||||
|
||||
// Prevent stack overflow from deeply nested groups (DoS protection)
|
||||
if (depth > MAX_NESTED_GROUP_DEPTH) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let tokenizer = new Tokenizer(str);
|
||||
let tokens = tokenizer.tokenize();
|
||||
|
||||
let addresses = [];
|
||||
let address = [];
|
||||
let parsedAddresses = [];
|
||||
|
||||
tokens.forEach(token => {
|
||||
if (token.type === 'operator' && (token.value === ',' || token.value === ';')) {
|
||||
if (address.length) {
|
||||
addresses.push(address);
|
||||
}
|
||||
address = [];
|
||||
} else {
|
||||
address.push(token);
|
||||
}
|
||||
});
|
||||
|
||||
if (address.length) {
|
||||
addresses.push(address);
|
||||
}
|
||||
|
||||
addresses.forEach(address => {
|
||||
address = _handleAddress(address, depth);
|
||||
if (address.length) {
|
||||
parsedAddresses = parsedAddresses.concat(address);
|
||||
}
|
||||
});
|
||||
|
||||
if (options.flatten) {
|
||||
let addresses = [];
|
||||
let walkAddressList = list => {
|
||||
list.forEach(address => {
|
||||
if (address.group) {
|
||||
return walkAddressList(address.group);
|
||||
} else {
|
||||
addresses.push(address);
|
||||
}
|
||||
});
|
||||
};
|
||||
walkAddressList(parsedAddresses);
|
||||
return addresses;
|
||||
}
|
||||
|
||||
return parsedAddresses;
|
||||
}
|
||||
|
||||
// expose to the world
|
||||
module.exports = addressparser;
|
||||
139
backend/node_modules/imapflow/node_modules/nodemailer/lib/base64/index.js
generated
vendored
Normal file
139
backend/node_modules/imapflow/node_modules/nodemailer/lib/base64/index.js
generated
vendored
Normal file
@@ -0,0 +1,139 @@
|
||||
'use strict';
|
||||
|
||||
const Transform = require('stream').Transform;
|
||||
|
||||
/**
|
||||
* Encodes a Buffer into a base64 encoded string
|
||||
*
|
||||
* @param {Buffer} buffer Buffer to convert
|
||||
* @returns {String} base64 encoded string
|
||||
*/
|
||||
function encode(buffer) {
|
||||
if (typeof buffer === 'string') {
|
||||
buffer = Buffer.from(buffer, 'utf-8');
|
||||
}
|
||||
|
||||
return buffer.toString('base64');
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds soft line breaks to a base64 string
|
||||
*
|
||||
* @param {String} str base64 encoded string that might need line wrapping
|
||||
* @param {Number} [lineLength=76] Maximum allowed length for a line
|
||||
* @returns {String} Soft-wrapped base64 encoded string
|
||||
*/
|
||||
function wrap(str, lineLength) {
|
||||
str = (str || '').toString();
|
||||
lineLength = lineLength || 76;
|
||||
|
||||
if (str.length <= lineLength) {
|
||||
return str;
|
||||
}
|
||||
|
||||
let result = [];
|
||||
let pos = 0;
|
||||
let chunkLength = lineLength * 1024;
|
||||
while (pos < str.length) {
|
||||
let wrappedLines = str.substr(pos, chunkLength).replace(new RegExp('.{' + lineLength + '}', 'g'), '$&\r\n');
|
||||
result.push(wrappedLines);
|
||||
pos += chunkLength;
|
||||
}
|
||||
|
||||
return result.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a transform stream for encoding data to base64 encoding
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} options Stream options
|
||||
* @param {Number} [options.lineLength=76] Maximum length for lines, set to false to disable wrapping
|
||||
*/
|
||||
class Encoder extends Transform {
|
||||
constructor(options) {
|
||||
super();
|
||||
this.options = options || {};
|
||||
|
||||
if (this.options.lineLength !== false) {
|
||||
this.options.lineLength = this.options.lineLength || 76;
|
||||
}
|
||||
|
||||
this._curLine = '';
|
||||
this._remainingBytes = false;
|
||||
|
||||
this.inputBytes = 0;
|
||||
this.outputBytes = 0;
|
||||
}
|
||||
|
||||
_transform(chunk, encoding, done) {
|
||||
if (encoding !== 'buffer') {
|
||||
chunk = Buffer.from(chunk, encoding);
|
||||
}
|
||||
|
||||
if (!chunk || !chunk.length) {
|
||||
return setImmediate(done);
|
||||
}
|
||||
|
||||
this.inputBytes += chunk.length;
|
||||
|
||||
if (this._remainingBytes && this._remainingBytes.length) {
|
||||
chunk = Buffer.concat([this._remainingBytes, chunk], this._remainingBytes.length + chunk.length);
|
||||
this._remainingBytes = false;
|
||||
}
|
||||
|
||||
if (chunk.length % 3) {
|
||||
this._remainingBytes = chunk.slice(chunk.length - (chunk.length % 3));
|
||||
chunk = chunk.slice(0, chunk.length - (chunk.length % 3));
|
||||
} else {
|
||||
this._remainingBytes = false;
|
||||
}
|
||||
|
||||
let b64 = this._curLine + encode(chunk);
|
||||
|
||||
if (this.options.lineLength) {
|
||||
b64 = wrap(b64, this.options.lineLength);
|
||||
|
||||
let lastLF = b64.lastIndexOf('\n');
|
||||
if (lastLF < 0) {
|
||||
this._curLine = b64;
|
||||
b64 = '';
|
||||
} else {
|
||||
this._curLine = b64.substring(lastLF + 1);
|
||||
b64 = b64.substring(0, lastLF + 1);
|
||||
|
||||
if (b64 && !b64.endsWith('\r\n')) {
|
||||
b64 += '\r\n';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this._curLine = '';
|
||||
}
|
||||
|
||||
if (b64) {
|
||||
this.outputBytes += b64.length;
|
||||
this.push(Buffer.from(b64, 'ascii'));
|
||||
}
|
||||
|
||||
setImmediate(done);
|
||||
}
|
||||
|
||||
_flush(done) {
|
||||
if (this._remainingBytes && this._remainingBytes.length) {
|
||||
this._curLine += encode(this._remainingBytes);
|
||||
}
|
||||
|
||||
if (this._curLine) {
|
||||
this.outputBytes += this._curLine.length;
|
||||
this.push(Buffer.from(this._curLine, 'ascii'));
|
||||
this._curLine = '';
|
||||
}
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
encode,
|
||||
wrap,
|
||||
Encoder
|
||||
};
|
||||
253
backend/node_modules/imapflow/node_modules/nodemailer/lib/dkim/index.js
generated
vendored
Normal file
253
backend/node_modules/imapflow/node_modules/nodemailer/lib/dkim/index.js
generated
vendored
Normal file
@@ -0,0 +1,253 @@
|
||||
'use strict';
|
||||
|
||||
// FIXME:
|
||||
// replace this Transform mess with a method that pipes input argument to output argument
|
||||
|
||||
const MessageParser = require('./message-parser');
|
||||
const RelaxedBody = require('./relaxed-body');
|
||||
const sign = require('./sign');
|
||||
const PassThrough = require('stream').PassThrough;
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const DKIM_ALGO = 'sha256';
|
||||
const MAX_MESSAGE_SIZE = 2 * 1024 * 1024; // buffer messages larger than this to disk
|
||||
|
||||
/*
|
||||
// Usage:
|
||||
|
||||
let dkim = new DKIM({
|
||||
domainName: 'example.com',
|
||||
keySelector: 'key-selector',
|
||||
privateKey,
|
||||
cacheDir: '/tmp'
|
||||
});
|
||||
dkim.sign(input).pipe(process.stdout);
|
||||
|
||||
// Where inputStream is a rfc822 message (either a stream, string or Buffer)
|
||||
// and outputStream is a DKIM signed rfc822 message
|
||||
*/
|
||||
|
||||
class DKIMSigner {
|
||||
constructor(options, keys, input, output) {
|
||||
this.options = options || {};
|
||||
this.keys = keys;
|
||||
|
||||
this.cacheTreshold = Number(this.options.cacheTreshold) || MAX_MESSAGE_SIZE;
|
||||
this.hashAlgo = this.options.hashAlgo || DKIM_ALGO;
|
||||
|
||||
this.cacheDir = this.options.cacheDir || false;
|
||||
|
||||
this.chunks = [];
|
||||
this.chunklen = 0;
|
||||
this.readPos = 0;
|
||||
this.cachePath = this.cacheDir
|
||||
? path.join(this.cacheDir, 'message.' + Date.now() + '-' + crypto.randomBytes(14).toString('hex'))
|
||||
: false;
|
||||
this.cache = false;
|
||||
|
||||
this.headers = false;
|
||||
this.bodyHash = false;
|
||||
this.parser = false;
|
||||
this.relaxedBody = false;
|
||||
|
||||
this.input = input;
|
||||
this.output = output;
|
||||
this.output.usingCache = false;
|
||||
|
||||
this.hasErrored = false;
|
||||
|
||||
this.input.on('error', err => {
|
||||
this.hasErrored = true;
|
||||
this.cleanup();
|
||||
output.emit('error', err);
|
||||
});
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if (!this.cache || !this.cachePath) {
|
||||
return;
|
||||
}
|
||||
fs.unlink(this.cachePath, () => false);
|
||||
}
|
||||
|
||||
createReadCache() {
|
||||
// pipe remainings to cache file
|
||||
this.cache = fs.createReadStream(this.cachePath);
|
||||
this.cache.once('error', err => {
|
||||
this.cleanup();
|
||||
this.output.emit('error', err);
|
||||
});
|
||||
this.cache.once('close', () => {
|
||||
this.cleanup();
|
||||
});
|
||||
this.cache.pipe(this.output);
|
||||
}
|
||||
|
||||
sendNextChunk() {
|
||||
if (this.hasErrored) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.readPos >= this.chunks.length) {
|
||||
if (!this.cache) {
|
||||
return this.output.end();
|
||||
}
|
||||
return this.createReadCache();
|
||||
}
|
||||
let chunk = this.chunks[this.readPos++];
|
||||
if (this.output.write(chunk) === false) {
|
||||
return this.output.once('drain', () => {
|
||||
this.sendNextChunk();
|
||||
});
|
||||
}
|
||||
setImmediate(() => this.sendNextChunk());
|
||||
}
|
||||
|
||||
sendSignedOutput() {
|
||||
let keyPos = 0;
|
||||
let signNextKey = () => {
|
||||
if (keyPos >= this.keys.length) {
|
||||
this.output.write(this.parser.rawHeaders);
|
||||
return setImmediate(() => this.sendNextChunk());
|
||||
}
|
||||
let key = this.keys[keyPos++];
|
||||
let dkimField = sign(this.headers, this.hashAlgo, this.bodyHash, {
|
||||
domainName: key.domainName,
|
||||
keySelector: key.keySelector,
|
||||
privateKey: key.privateKey,
|
||||
headerFieldNames: this.options.headerFieldNames,
|
||||
skipFields: this.options.skipFields
|
||||
});
|
||||
if (dkimField) {
|
||||
this.output.write(Buffer.from(dkimField + '\r\n'));
|
||||
}
|
||||
return setImmediate(signNextKey);
|
||||
};
|
||||
|
||||
if (this.bodyHash && this.headers) {
|
||||
return signNextKey();
|
||||
}
|
||||
|
||||
this.output.write(this.parser.rawHeaders);
|
||||
this.sendNextChunk();
|
||||
}
|
||||
|
||||
createWriteCache() {
|
||||
this.output.usingCache = true;
|
||||
// pipe remainings to cache file
|
||||
this.cache = fs.createWriteStream(this.cachePath);
|
||||
this.cache.once('error', err => {
|
||||
this.cleanup();
|
||||
// drain input
|
||||
this.relaxedBody.unpipe(this.cache);
|
||||
this.relaxedBody.on('readable', () => {
|
||||
while (this.relaxedBody.read() !== null) {
|
||||
// do nothing
|
||||
}
|
||||
});
|
||||
this.hasErrored = true;
|
||||
// emit error
|
||||
this.output.emit('error', err);
|
||||
});
|
||||
this.cache.once('close', () => {
|
||||
this.sendSignedOutput();
|
||||
});
|
||||
this.relaxedBody.removeAllListeners('readable');
|
||||
this.relaxedBody.pipe(this.cache);
|
||||
}
|
||||
|
||||
signStream() {
|
||||
this.parser = new MessageParser();
|
||||
this.relaxedBody = new RelaxedBody({
|
||||
hashAlgo: this.hashAlgo
|
||||
});
|
||||
|
||||
this.parser.on('headers', value => {
|
||||
this.headers = value;
|
||||
});
|
||||
|
||||
this.relaxedBody.on('hash', value => {
|
||||
this.bodyHash = value;
|
||||
});
|
||||
|
||||
this.relaxedBody.on('readable', () => {
|
||||
let chunk;
|
||||
if (this.cache) {
|
||||
return;
|
||||
}
|
||||
while ((chunk = this.relaxedBody.read()) !== null) {
|
||||
this.chunks.push(chunk);
|
||||
this.chunklen += chunk.length;
|
||||
if (this.chunklen >= this.cacheTreshold && this.cachePath) {
|
||||
return this.createWriteCache();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.relaxedBody.on('end', () => {
|
||||
if (this.cache) {
|
||||
return;
|
||||
}
|
||||
this.sendSignedOutput();
|
||||
});
|
||||
|
||||
this.parser.pipe(this.relaxedBody);
|
||||
setImmediate(() => this.input.pipe(this.parser));
|
||||
}
|
||||
}
|
||||
|
||||
class DKIM {
|
||||
constructor(options) {
|
||||
this.options = options || {};
|
||||
this.keys = [].concat(
|
||||
this.options.keys || {
|
||||
domainName: options.domainName,
|
||||
keySelector: options.keySelector,
|
||||
privateKey: options.privateKey
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
sign(input, extraOptions) {
|
||||
let output = new PassThrough();
|
||||
let inputStream = input;
|
||||
let writeValue = false;
|
||||
|
||||
if (Buffer.isBuffer(input)) {
|
||||
writeValue = input;
|
||||
inputStream = new PassThrough();
|
||||
} else if (typeof input === 'string') {
|
||||
writeValue = Buffer.from(input);
|
||||
inputStream = new PassThrough();
|
||||
}
|
||||
|
||||
let options = this.options;
|
||||
if (extraOptions && Object.keys(extraOptions).length) {
|
||||
options = {};
|
||||
Object.keys(this.options || {}).forEach(key => {
|
||||
options[key] = this.options[key];
|
||||
});
|
||||
Object.keys(extraOptions || {}).forEach(key => {
|
||||
if (!(key in options)) {
|
||||
options[key] = extraOptions[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let signer = new DKIMSigner(options, this.keys, inputStream, output);
|
||||
setImmediate(() => {
|
||||
signer.signStream();
|
||||
if (writeValue) {
|
||||
setImmediate(() => {
|
||||
inputStream.end(writeValue);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DKIM;
|
||||
155
backend/node_modules/imapflow/node_modules/nodemailer/lib/dkim/message-parser.js
generated
vendored
Normal file
155
backend/node_modules/imapflow/node_modules/nodemailer/lib/dkim/message-parser.js
generated
vendored
Normal file
@@ -0,0 +1,155 @@
|
||||
'use strict';
|
||||
|
||||
const Transform = require('stream').Transform;
|
||||
|
||||
/**
|
||||
* MessageParser instance is a transform stream that separates message headers
|
||||
* from the rest of the body. Headers are emitted with the 'headers' event. Message
|
||||
* body is passed on as the resulting stream.
|
||||
*/
|
||||
class MessageParser extends Transform {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
this.lastBytes = Buffer.alloc(4);
|
||||
this.headersParsed = false;
|
||||
this.headerBytes = 0;
|
||||
this.headerChunks = [];
|
||||
this.rawHeaders = false;
|
||||
this.bodySize = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps count of the last 4 bytes in order to detect line breaks on chunk boundaries
|
||||
*
|
||||
* @param {Buffer} data Next data chunk from the stream
|
||||
*/
|
||||
updateLastBytes(data) {
|
||||
let lblen = this.lastBytes.length;
|
||||
let nblen = Math.min(data.length, lblen);
|
||||
|
||||
// shift existing bytes
|
||||
for (let i = 0, len = lblen - nblen; i < len; i++) {
|
||||
this.lastBytes[i] = this.lastBytes[i + nblen];
|
||||
}
|
||||
|
||||
// add new bytes
|
||||
for (let i = 1; i <= nblen; i++) {
|
||||
this.lastBytes[lblen - i] = data[data.length - i];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and removes message headers from the remaining body. We want to keep
|
||||
* headers separated until final delivery to be able to modify these
|
||||
*
|
||||
* @param {Buffer} data Next chunk of data
|
||||
* @return {Boolean} Returns true if headers are already found or false otherwise
|
||||
*/
|
||||
checkHeaders(data) {
|
||||
if (this.headersParsed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let lblen = this.lastBytes.length;
|
||||
let headerPos = 0;
|
||||
this.curLinePos = 0;
|
||||
for (let i = 0, len = this.lastBytes.length + data.length; i < len; i++) {
|
||||
let chr;
|
||||
if (i < lblen) {
|
||||
chr = this.lastBytes[i];
|
||||
} else {
|
||||
chr = data[i - lblen];
|
||||
}
|
||||
if (chr === 0x0a && i) {
|
||||
let pr1 = i - 1 < lblen ? this.lastBytes[i - 1] : data[i - 1 - lblen];
|
||||
let pr2 = i > 1 ? (i - 2 < lblen ? this.lastBytes[i - 2] : data[i - 2 - lblen]) : false;
|
||||
if (pr1 === 0x0a) {
|
||||
this.headersParsed = true;
|
||||
headerPos = i - lblen + 1;
|
||||
this.headerBytes += headerPos;
|
||||
break;
|
||||
} else if (pr1 === 0x0d && pr2 === 0x0a) {
|
||||
this.headersParsed = true;
|
||||
headerPos = i - lblen + 1;
|
||||
this.headerBytes += headerPos;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.headersParsed) {
|
||||
this.headerChunks.push(data.slice(0, headerPos));
|
||||
this.rawHeaders = Buffer.concat(this.headerChunks, this.headerBytes);
|
||||
this.headerChunks = null;
|
||||
this.emit('headers', this.parseHeaders());
|
||||
if (data.length - 1 > headerPos) {
|
||||
let chunk = data.slice(headerPos);
|
||||
this.bodySize += chunk.length;
|
||||
// this would be the first chunk of data sent downstream
|
||||
setImmediate(() => this.push(chunk));
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
this.headerBytes += data.length;
|
||||
this.headerChunks.push(data);
|
||||
}
|
||||
|
||||
// store last 4 bytes to catch header break
|
||||
this.updateLastBytes(data);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
_transform(chunk, encoding, callback) {
|
||||
if (!chunk || !chunk.length) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
if (typeof chunk === 'string') {
|
||||
chunk = Buffer.from(chunk, encoding);
|
||||
}
|
||||
|
||||
let headersFound;
|
||||
|
||||
try {
|
||||
headersFound = this.checkHeaders(chunk);
|
||||
} catch (E) {
|
||||
return callback(E);
|
||||
}
|
||||
|
||||
if (headersFound) {
|
||||
this.bodySize += chunk.length;
|
||||
this.push(chunk);
|
||||
}
|
||||
|
||||
setImmediate(callback);
|
||||
}
|
||||
|
||||
_flush(callback) {
|
||||
if (this.headerChunks) {
|
||||
let chunk = Buffer.concat(this.headerChunks, this.headerBytes);
|
||||
this.bodySize += chunk.length;
|
||||
this.push(chunk);
|
||||
this.headerChunks = null;
|
||||
}
|
||||
callback();
|
||||
}
|
||||
|
||||
parseHeaders() {
|
||||
let lines = (this.rawHeaders || '').toString().split(/\r?\n/);
|
||||
for (let i = lines.length - 1; i > 0; i--) {
|
||||
if (/^\s/.test(lines[i])) {
|
||||
lines[i - 1] += '\n' + lines[i];
|
||||
lines.splice(i, 1);
|
||||
}
|
||||
}
|
||||
return lines
|
||||
.filter(line => line.trim())
|
||||
.map(line => ({
|
||||
key: line.substr(0, line.indexOf(':')).trim().toLowerCase(),
|
||||
line
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MessageParser;
|
||||
154
backend/node_modules/imapflow/node_modules/nodemailer/lib/dkim/relaxed-body.js
generated
vendored
Normal file
154
backend/node_modules/imapflow/node_modules/nodemailer/lib/dkim/relaxed-body.js
generated
vendored
Normal file
@@ -0,0 +1,154 @@
|
||||
'use strict';
|
||||
|
||||
// streams through a message body and calculates relaxed body hash
|
||||
|
||||
const Transform = require('stream').Transform;
|
||||
const crypto = require('crypto');
|
||||
|
||||
class RelaxedBody extends Transform {
|
||||
constructor(options) {
|
||||
super();
|
||||
options = options || {};
|
||||
this.chunkBuffer = [];
|
||||
this.chunkBufferLen = 0;
|
||||
this.bodyHash = crypto.createHash(options.hashAlgo || 'sha1');
|
||||
this.remainder = '';
|
||||
this.byteLength = 0;
|
||||
|
||||
this.debug = options.debug;
|
||||
this._debugBody = options.debug ? [] : false;
|
||||
}
|
||||
|
||||
updateHash(chunk) {
|
||||
let bodyStr;
|
||||
|
||||
// find next remainder
|
||||
let nextRemainder = '';
|
||||
|
||||
// This crux finds and removes the spaces from the last line and the newline characters after the last non-empty line
|
||||
// If we get another chunk that does not match this description then we can restore the previously processed data
|
||||
let state = 'file';
|
||||
for (let i = chunk.length - 1; i >= 0; i--) {
|
||||
let c = chunk[i];
|
||||
|
||||
if (state === 'file' && (c === 0x0a || c === 0x0d)) {
|
||||
// do nothing, found \n or \r at the end of chunk, stil end of file
|
||||
} else if (state === 'file' && (c === 0x09 || c === 0x20)) {
|
||||
// switch to line ending mode, this is the last non-empty line
|
||||
state = 'line';
|
||||
} else if (state === 'line' && (c === 0x09 || c === 0x20)) {
|
||||
// do nothing, found ' ' or \t at the end of line, keep processing the last non-empty line
|
||||
} else if (state === 'file' || state === 'line') {
|
||||
// non line/file ending character found, switch to body mode
|
||||
state = 'body';
|
||||
if (i === chunk.length - 1) {
|
||||
// final char is not part of line end or file end, so do nothing
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (i === 0) {
|
||||
// reached to the beginning of the chunk, check if it is still about the ending
|
||||
// and if the remainder also matches
|
||||
if (
|
||||
(state === 'file' && (!this.remainder || /[\r\n]$/.test(this.remainder))) ||
|
||||
(state === 'line' && (!this.remainder || /[ \t]$/.test(this.remainder)))
|
||||
) {
|
||||
// keep everything
|
||||
this.remainder += chunk.toString('binary');
|
||||
return;
|
||||
} else if (state === 'line' || state === 'file') {
|
||||
// process existing remainder as normal line but store the current chunk
|
||||
nextRemainder = chunk.toString('binary');
|
||||
chunk = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (state !== 'body') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// reached first non ending byte
|
||||
nextRemainder = chunk.slice(i + 1).toString('binary');
|
||||
chunk = chunk.slice(0, i + 1);
|
||||
break;
|
||||
}
|
||||
|
||||
let needsFixing = !!this.remainder;
|
||||
if (chunk && !needsFixing) {
|
||||
// check if we even need to change anything
|
||||
for (let i = 0, len = chunk.length; i < len; i++) {
|
||||
if (i && chunk[i] === 0x0a && chunk[i - 1] !== 0x0d) {
|
||||
// missing \r before \n
|
||||
needsFixing = true;
|
||||
break;
|
||||
} else if (i && chunk[i] === 0x0d && chunk[i - 1] === 0x20) {
|
||||
// trailing WSP found
|
||||
needsFixing = true;
|
||||
break;
|
||||
} else if (i && chunk[i] === 0x20 && chunk[i - 1] === 0x20) {
|
||||
// multiple spaces found, needs to be replaced with just one
|
||||
needsFixing = true;
|
||||
break;
|
||||
} else if (chunk[i] === 0x09) {
|
||||
// TAB found, needs to be replaced with a space
|
||||
needsFixing = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (needsFixing) {
|
||||
bodyStr = this.remainder + (chunk ? chunk.toString('binary') : '');
|
||||
this.remainder = nextRemainder;
|
||||
bodyStr = bodyStr
|
||||
.replace(/\r?\n/g, '\n') // use js line endings
|
||||
.replace(/[ \t]*$/gm, '') // remove line endings, rtrim
|
||||
.replace(/[ \t]+/gm, ' ') // single spaces
|
||||
.replace(/\n/g, '\r\n'); // restore rfc822 line endings
|
||||
chunk = Buffer.from(bodyStr, 'binary');
|
||||
} else if (nextRemainder) {
|
||||
this.remainder = nextRemainder;
|
||||
}
|
||||
|
||||
if (this.debug) {
|
||||
this._debugBody.push(chunk);
|
||||
}
|
||||
this.bodyHash.update(chunk);
|
||||
}
|
||||
|
||||
_transform(chunk, encoding, callback) {
|
||||
if (!chunk || !chunk.length) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
if (typeof chunk === 'string') {
|
||||
chunk = Buffer.from(chunk, encoding);
|
||||
}
|
||||
|
||||
this.updateHash(chunk);
|
||||
|
||||
this.byteLength += chunk.length;
|
||||
this.push(chunk);
|
||||
callback();
|
||||
}
|
||||
|
||||
_flush(callback) {
|
||||
// generate final hash and emit it
|
||||
if (/[\r\n]$/.test(this.remainder) && this.byteLength > 2) {
|
||||
// add terminating line end
|
||||
this.bodyHash.update(Buffer.from('\r\n'));
|
||||
}
|
||||
if (!this.byteLength) {
|
||||
// emit empty line buffer to keep the stream flowing
|
||||
this.push(Buffer.from('\r\n'));
|
||||
// this.bodyHash.update(Buffer.from('\r\n'));
|
||||
}
|
||||
|
||||
this.emit('hash', this.bodyHash.digest('base64'), this.debug ? Buffer.concat(this._debugBody) : false);
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RelaxedBody;
|
||||
117
backend/node_modules/imapflow/node_modules/nodemailer/lib/dkim/sign.js
generated
vendored
Normal file
117
backend/node_modules/imapflow/node_modules/nodemailer/lib/dkim/sign.js
generated
vendored
Normal file
@@ -0,0 +1,117 @@
|
||||
'use strict';
|
||||
|
||||
const punycode = require('../punycode');
|
||||
const mimeFuncs = require('../mime-funcs');
|
||||
const crypto = require('crypto');
|
||||
|
||||
/**
|
||||
* Returns DKIM signature header line
|
||||
*
|
||||
* @param {Object} headers Parsed headers object from MessageParser
|
||||
* @param {String} bodyHash Base64 encoded hash of the message
|
||||
* @param {Object} options DKIM options
|
||||
* @param {String} options.domainName Domain name to be signed for
|
||||
* @param {String} options.keySelector DKIM key selector to use
|
||||
* @param {String} options.privateKey DKIM private key to use
|
||||
* @return {String} Complete header line
|
||||
*/
|
||||
|
||||
module.exports = (headers, hashAlgo, bodyHash, options) => {
|
||||
options = options || {};
|
||||
|
||||
// all listed fields from RFC4871 #5.5
|
||||
let defaultFieldNames =
|
||||
'From:Sender:Reply-To:Subject:Date:Message-ID:To:' +
|
||||
'Cc:MIME-Version:Content-Type:Content-Transfer-Encoding:Content-ID:' +
|
||||
'Content-Description:Resent-Date:Resent-From:Resent-Sender:' +
|
||||
'Resent-To:Resent-Cc:Resent-Message-ID:In-Reply-To:References:' +
|
||||
'List-Id:List-Help:List-Unsubscribe:List-Subscribe:List-Post:' +
|
||||
'List-Owner:List-Archive';
|
||||
|
||||
let fieldNames = options.headerFieldNames || defaultFieldNames;
|
||||
|
||||
let canonicalizedHeaderData = relaxedHeaders(headers, fieldNames, options.skipFields);
|
||||
let dkimHeader = generateDKIMHeader(options.domainName, options.keySelector, canonicalizedHeaderData.fieldNames, hashAlgo, bodyHash);
|
||||
|
||||
let signer, signature;
|
||||
|
||||
canonicalizedHeaderData.headers += 'dkim-signature:' + relaxedHeaderLine(dkimHeader);
|
||||
|
||||
signer = crypto.createSign(('rsa-' + hashAlgo).toUpperCase());
|
||||
signer.update(canonicalizedHeaderData.headers);
|
||||
try {
|
||||
signature = signer.sign(options.privateKey, 'base64');
|
||||
} catch (_E) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return dkimHeader + signature.replace(/(^.{73}|.{75}(?!\r?\n|\r))/g, '$&\r\n ').trim();
|
||||
};
|
||||
|
||||
module.exports.relaxedHeaders = relaxedHeaders;
|
||||
|
||||
function generateDKIMHeader(domainName, keySelector, fieldNames, hashAlgo, bodyHash) {
|
||||
let dkim = [
|
||||
'v=1',
|
||||
'a=rsa-' + hashAlgo,
|
||||
'c=relaxed/relaxed',
|
||||
'd=' + punycode.toASCII(domainName),
|
||||
'q=dns/txt',
|
||||
's=' + keySelector,
|
||||
'bh=' + bodyHash,
|
||||
'h=' + fieldNames
|
||||
].join('; ');
|
||||
|
||||
return mimeFuncs.foldLines('DKIM-Signature: ' + dkim, 76) + ';\r\n b=';
|
||||
}
|
||||
|
||||
function relaxedHeaders(headers, fieldNames, skipFields) {
|
||||
let includedFields = new Set();
|
||||
let skip = new Set();
|
||||
let headerFields = new Map();
|
||||
|
||||
(skipFields || '')
|
||||
.toLowerCase()
|
||||
.split(':')
|
||||
.forEach(field => {
|
||||
skip.add(field.trim());
|
||||
});
|
||||
|
||||
(fieldNames || '')
|
||||
.toLowerCase()
|
||||
.split(':')
|
||||
.filter(field => !skip.has(field.trim()))
|
||||
.forEach(field => {
|
||||
includedFields.add(field.trim());
|
||||
});
|
||||
|
||||
for (let i = headers.length - 1; i >= 0; i--) {
|
||||
let line = headers[i];
|
||||
// only include the first value from bottom to top
|
||||
if (includedFields.has(line.key) && !headerFields.has(line.key)) {
|
||||
headerFields.set(line.key, relaxedHeaderLine(line.line));
|
||||
}
|
||||
}
|
||||
|
||||
let headersList = [];
|
||||
let fields = [];
|
||||
includedFields.forEach(field => {
|
||||
if (headerFields.has(field)) {
|
||||
fields.push(field);
|
||||
headersList.push(field + ':' + headerFields.get(field));
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
headers: headersList.join('\r\n') + '\r\n',
|
||||
fieldNames: fields.join(':')
|
||||
};
|
||||
}
|
||||
|
||||
function relaxedHeaderLine(line) {
|
||||
return line
|
||||
.substr(line.indexOf(':') + 1)
|
||||
.replace(/\r?\n/g, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
281
backend/node_modules/imapflow/node_modules/nodemailer/lib/fetch/cookies.js
generated
vendored
Normal file
281
backend/node_modules/imapflow/node_modules/nodemailer/lib/fetch/cookies.js
generated
vendored
Normal file
@@ -0,0 +1,281 @@
|
||||
'use strict';
|
||||
|
||||
// module to handle cookies
|
||||
|
||||
const urllib = require('url');
|
||||
|
||||
const SESSION_TIMEOUT = 1800; // 30 min
|
||||
|
||||
/**
|
||||
* Creates a biskviit cookie jar for managing cookie values in memory
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} [options] Optional options object
|
||||
*/
|
||||
class Cookies {
|
||||
constructor(options) {
|
||||
this.options = options || {};
|
||||
this.cookies = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a cookie string to the cookie storage
|
||||
*
|
||||
* @param {String} cookieStr Value from the 'Set-Cookie:' header
|
||||
* @param {String} url Current URL
|
||||
*/
|
||||
set(cookieStr, url) {
|
||||
let urlparts = urllib.parse(url || '');
|
||||
let cookie = this.parse(cookieStr);
|
||||
let domain;
|
||||
|
||||
if (cookie.domain) {
|
||||
domain = cookie.domain.replace(/^\./, '');
|
||||
|
||||
// do not allow cross origin cookies
|
||||
if (
|
||||
// can't be valid if the requested domain is shorter than current hostname
|
||||
urlparts.hostname.length < domain.length ||
|
||||
// prefix domains with dot to be sure that partial matches are not used
|
||||
('.' + urlparts.hostname).substr(-domain.length + 1) !== '.' + domain
|
||||
) {
|
||||
cookie.domain = urlparts.hostname;
|
||||
}
|
||||
} else {
|
||||
cookie.domain = urlparts.hostname;
|
||||
}
|
||||
|
||||
if (!cookie.path) {
|
||||
cookie.path = this.getPath(urlparts.pathname);
|
||||
}
|
||||
|
||||
// if no expire date, then use sessionTimeout value
|
||||
if (!cookie.expires) {
|
||||
cookie.expires = new Date(Date.now() + (Number(this.options.sessionTimeout || SESSION_TIMEOUT) || SESSION_TIMEOUT) * 1000);
|
||||
}
|
||||
|
||||
return this.add(cookie);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns cookie string for the 'Cookie:' header.
|
||||
*
|
||||
* @param {String} url URL to check for
|
||||
* @returns {String} Cookie header or empty string if no matches were found
|
||||
*/
|
||||
get(url) {
|
||||
return this.list(url)
|
||||
.map(cookie => cookie.name + '=' + cookie.value)
|
||||
.join('; ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all valied cookie objects for the specified URL
|
||||
*
|
||||
* @param {String} url URL to check for
|
||||
* @returns {Array} An array of cookie objects
|
||||
*/
|
||||
list(url) {
|
||||
let result = [];
|
||||
let i;
|
||||
let cookie;
|
||||
|
||||
for (i = this.cookies.length - 1; i >= 0; i--) {
|
||||
cookie = this.cookies[i];
|
||||
|
||||
if (this.isExpired(cookie)) {
|
||||
this.cookies.splice(i, i);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.match(cookie, url)) {
|
||||
result.unshift(cookie);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses cookie string from the 'Set-Cookie:' header
|
||||
*
|
||||
* @param {String} cookieStr String from the 'Set-Cookie:' header
|
||||
* @returns {Object} Cookie object
|
||||
*/
|
||||
parse(cookieStr) {
|
||||
let cookie = {};
|
||||
|
||||
(cookieStr || '')
|
||||
.toString()
|
||||
.split(';')
|
||||
.forEach(cookiePart => {
|
||||
let valueParts = cookiePart.split('=');
|
||||
let key = valueParts.shift().trim().toLowerCase();
|
||||
let value = valueParts.join('=').trim();
|
||||
let domain;
|
||||
|
||||
if (!key) {
|
||||
// skip empty parts
|
||||
return;
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case 'expires':
|
||||
value = new Date(value);
|
||||
// ignore date if can not parse it
|
||||
if (value.toString() !== 'Invalid Date') {
|
||||
cookie.expires = value;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'path':
|
||||
cookie.path = value;
|
||||
break;
|
||||
|
||||
case 'domain':
|
||||
domain = value.toLowerCase();
|
||||
if (domain.length && domain.charAt(0) !== '.') {
|
||||
domain = '.' + domain; // ensure preceeding dot for user set domains
|
||||
}
|
||||
cookie.domain = domain;
|
||||
break;
|
||||
|
||||
case 'max-age':
|
||||
cookie.expires = new Date(Date.now() + (Number(value) || 0) * 1000);
|
||||
break;
|
||||
|
||||
case 'secure':
|
||||
cookie.secure = true;
|
||||
break;
|
||||
|
||||
case 'httponly':
|
||||
cookie.httponly = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
if (!cookie.name) {
|
||||
cookie.name = key;
|
||||
cookie.value = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return cookie;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a cookie object is valid for a specified URL
|
||||
*
|
||||
* @param {Object} cookie Cookie object
|
||||
* @param {String} url URL to check for
|
||||
* @returns {Boolean} true if cookie is valid for specifiec URL
|
||||
*/
|
||||
match(cookie, url) {
|
||||
let urlparts = urllib.parse(url || '');
|
||||
|
||||
// check if hostname matches
|
||||
// .foo.com also matches subdomains, foo.com does not
|
||||
if (
|
||||
urlparts.hostname !== cookie.domain &&
|
||||
(cookie.domain.charAt(0) !== '.' || ('.' + urlparts.hostname).substr(-cookie.domain.length) !== cookie.domain)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if path matches
|
||||
let path = this.getPath(urlparts.pathname);
|
||||
if (path.substr(0, cookie.path.length) !== cookie.path) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check secure argument
|
||||
if (cookie.secure && urlparts.protocol !== 'https:') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds (or updates/removes if needed) a cookie object to the cookie storage
|
||||
*
|
||||
* @param {Object} cookie Cookie value to be stored
|
||||
*/
|
||||
add(cookie) {
|
||||
let i;
|
||||
let len;
|
||||
|
||||
// nothing to do here
|
||||
if (!cookie || !cookie.name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// overwrite if has same params
|
||||
for (i = 0, len = this.cookies.length; i < len; i++) {
|
||||
if (this.compare(this.cookies[i], cookie)) {
|
||||
// check if the cookie needs to be removed instead
|
||||
if (this.isExpired(cookie)) {
|
||||
this.cookies.splice(i, 1); // remove expired/unset cookie
|
||||
return false;
|
||||
}
|
||||
|
||||
this.cookies[i] = cookie;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// add as new if not already expired
|
||||
if (!this.isExpired(cookie)) {
|
||||
this.cookies.push(cookie);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if two cookie objects are the same
|
||||
*
|
||||
* @param {Object} a Cookie to check against
|
||||
* @param {Object} b Cookie to check against
|
||||
* @returns {Boolean} True, if the cookies are the same
|
||||
*/
|
||||
compare(a, b) {
|
||||
return a.name === b.name && a.path === b.path && a.domain === b.domain && a.secure === b.secure && a.httponly === a.httponly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a cookie is expired
|
||||
*
|
||||
* @param {Object} cookie Cookie object to check against
|
||||
* @returns {Boolean} True, if the cookie is expired
|
||||
*/
|
||||
isExpired(cookie) {
|
||||
return (cookie.expires && cookie.expires < new Date()) || !cookie.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns normalized cookie path for an URL path argument
|
||||
*
|
||||
* @param {String} pathname
|
||||
* @returns {String} Normalized path
|
||||
*/
|
||||
getPath(pathname) {
|
||||
let path = (pathname || '/').split('/');
|
||||
path.pop(); // remove filename part
|
||||
path = path.join('/').trim();
|
||||
|
||||
// ensure path prefix /
|
||||
if (path.charAt(0) !== '/') {
|
||||
path = '/' + path;
|
||||
}
|
||||
|
||||
// ensure path suffix /
|
||||
if (path.substr(-1) !== '/') {
|
||||
path += '/';
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Cookies;
|
||||
280
backend/node_modules/imapflow/node_modules/nodemailer/lib/fetch/index.js
generated
vendored
Normal file
280
backend/node_modules/imapflow/node_modules/nodemailer/lib/fetch/index.js
generated
vendored
Normal file
@@ -0,0 +1,280 @@
|
||||
'use strict';
|
||||
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const urllib = require('url');
|
||||
const zlib = require('zlib');
|
||||
const PassThrough = require('stream').PassThrough;
|
||||
const Cookies = require('./cookies');
|
||||
const packageData = require('../../package.json');
|
||||
const net = require('net');
|
||||
|
||||
const MAX_REDIRECTS = 5;
|
||||
|
||||
module.exports = function (url, options) {
|
||||
return nmfetch(url, options);
|
||||
};
|
||||
|
||||
module.exports.Cookies = Cookies;
|
||||
|
||||
function nmfetch(url, options) {
|
||||
options = options || {};
|
||||
|
||||
options.fetchRes = options.fetchRes || new PassThrough();
|
||||
options.cookies = options.cookies || new Cookies();
|
||||
options.redirects = options.redirects || 0;
|
||||
options.maxRedirects = isNaN(options.maxRedirects) ? MAX_REDIRECTS : options.maxRedirects;
|
||||
|
||||
if (options.cookie) {
|
||||
[].concat(options.cookie || []).forEach(cookie => {
|
||||
options.cookies.set(cookie, url);
|
||||
});
|
||||
options.cookie = false;
|
||||
}
|
||||
|
||||
let fetchRes = options.fetchRes;
|
||||
let parsed = urllib.parse(url);
|
||||
let method = (options.method || '').toString().trim().toUpperCase() || 'GET';
|
||||
let finished = false;
|
||||
let cookies;
|
||||
let body;
|
||||
|
||||
let handler = parsed.protocol === 'https:' ? https : http;
|
||||
|
||||
let headers = {
|
||||
'accept-encoding': 'gzip,deflate',
|
||||
'user-agent': 'nodemailer/' + packageData.version
|
||||
};
|
||||
|
||||
Object.keys(options.headers || {}).forEach(key => {
|
||||
headers[key.toLowerCase().trim()] = options.headers[key];
|
||||
});
|
||||
|
||||
if (options.userAgent) {
|
||||
headers['user-agent'] = options.userAgent;
|
||||
}
|
||||
|
||||
if (parsed.auth) {
|
||||
headers.Authorization = 'Basic ' + Buffer.from(parsed.auth).toString('base64');
|
||||
}
|
||||
|
||||
if ((cookies = options.cookies.get(url))) {
|
||||
headers.cookie = cookies;
|
||||
}
|
||||
|
||||
if (options.body) {
|
||||
if (options.contentType !== false) {
|
||||
headers['Content-Type'] = options.contentType || 'application/x-www-form-urlencoded';
|
||||
}
|
||||
|
||||
if (typeof options.body.pipe === 'function') {
|
||||
// it's a stream
|
||||
headers['Transfer-Encoding'] = 'chunked';
|
||||
body = options.body;
|
||||
body.on('error', err => {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
err.type = 'FETCH';
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
});
|
||||
} else {
|
||||
if (options.body instanceof Buffer) {
|
||||
body = options.body;
|
||||
} else if (typeof options.body === 'object') {
|
||||
try {
|
||||
// encodeURIComponent can fail on invalid input (partial emoji etc.)
|
||||
body = Buffer.from(
|
||||
Object.keys(options.body)
|
||||
.map(key => {
|
||||
let value = options.body[key].toString().trim();
|
||||
return encodeURIComponent(key) + '=' + encodeURIComponent(value);
|
||||
})
|
||||
.join('&')
|
||||
);
|
||||
} catch (E) {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
E.type = 'FETCH';
|
||||
E.sourceUrl = url;
|
||||
fetchRes.emit('error', E);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
body = Buffer.from(options.body.toString().trim());
|
||||
}
|
||||
|
||||
headers['Content-Type'] = options.contentType || 'application/x-www-form-urlencoded';
|
||||
headers['Content-Length'] = body.length;
|
||||
}
|
||||
// if method is not provided, use POST instead of GET
|
||||
method = (options.method || '').toString().trim().toUpperCase() || 'POST';
|
||||
}
|
||||
|
||||
let req;
|
||||
let reqOptions = {
|
||||
method,
|
||||
host: parsed.hostname,
|
||||
path: parsed.path,
|
||||
port: parsed.port ? parsed.port : parsed.protocol === 'https:' ? 443 : 80,
|
||||
headers,
|
||||
rejectUnauthorized: false,
|
||||
agent: false
|
||||
};
|
||||
|
||||
if (options.tls) {
|
||||
Object.keys(options.tls).forEach(key => {
|
||||
reqOptions[key] = options.tls[key];
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
parsed.protocol === 'https:' &&
|
||||
parsed.hostname &&
|
||||
parsed.hostname !== reqOptions.host &&
|
||||
!net.isIP(parsed.hostname) &&
|
||||
!reqOptions.servername
|
||||
) {
|
||||
reqOptions.servername = parsed.hostname;
|
||||
}
|
||||
|
||||
try {
|
||||
req = handler.request(reqOptions);
|
||||
} catch (E) {
|
||||
finished = true;
|
||||
setImmediate(() => {
|
||||
E.type = 'FETCH';
|
||||
E.sourceUrl = url;
|
||||
fetchRes.emit('error', E);
|
||||
});
|
||||
return fetchRes;
|
||||
}
|
||||
|
||||
if (options.timeout) {
|
||||
req.setTimeout(options.timeout, () => {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
req.abort();
|
||||
let err = new Error('Request Timeout');
|
||||
err.type = 'FETCH';
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
});
|
||||
}
|
||||
|
||||
req.on('error', err => {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
err.type = 'FETCH';
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
});
|
||||
|
||||
req.on('response', res => {
|
||||
let inflate;
|
||||
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (res.headers['content-encoding']) {
|
||||
case 'gzip':
|
||||
case 'deflate':
|
||||
inflate = zlib.createUnzip();
|
||||
break;
|
||||
}
|
||||
|
||||
if (res.headers['set-cookie']) {
|
||||
[].concat(res.headers['set-cookie'] || []).forEach(cookie => {
|
||||
options.cookies.set(cookie, url);
|
||||
});
|
||||
}
|
||||
|
||||
if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location) {
|
||||
// redirect
|
||||
options.redirects++;
|
||||
if (options.redirects > options.maxRedirects) {
|
||||
finished = true;
|
||||
let err = new Error('Maximum redirect count exceeded');
|
||||
err.type = 'FETCH';
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
req.abort();
|
||||
return;
|
||||
}
|
||||
// redirect does not include POST body
|
||||
options.method = 'GET';
|
||||
options.body = false;
|
||||
return nmfetch(urllib.resolve(url, res.headers.location), options);
|
||||
}
|
||||
|
||||
fetchRes.statusCode = res.statusCode;
|
||||
fetchRes.headers = res.headers;
|
||||
|
||||
if (res.statusCode >= 300 && !options.allowErrorResponse) {
|
||||
finished = true;
|
||||
let err = new Error('Invalid status code ' + res.statusCode);
|
||||
err.type = 'FETCH';
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
req.abort();
|
||||
return;
|
||||
}
|
||||
|
||||
res.on('error', err => {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
err.type = 'FETCH';
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
req.abort();
|
||||
});
|
||||
|
||||
if (inflate) {
|
||||
res.pipe(inflate).pipe(fetchRes);
|
||||
inflate.on('error', err => {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
err.type = 'FETCH';
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
req.abort();
|
||||
});
|
||||
} else {
|
||||
res.pipe(fetchRes);
|
||||
}
|
||||
});
|
||||
|
||||
setImmediate(() => {
|
||||
if (body) {
|
||||
try {
|
||||
if (typeof body.pipe === 'function') {
|
||||
return body.pipe(req);
|
||||
} else {
|
||||
req.write(body);
|
||||
}
|
||||
} catch (err) {
|
||||
finished = true;
|
||||
err.type = 'FETCH';
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
req.end();
|
||||
});
|
||||
|
||||
return fetchRes;
|
||||
}
|
||||
82
backend/node_modules/imapflow/node_modules/nodemailer/lib/json-transport/index.js
generated
vendored
Normal file
82
backend/node_modules/imapflow/node_modules/nodemailer/lib/json-transport/index.js
generated
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
'use strict';
|
||||
|
||||
const packageData = require('../../package.json');
|
||||
const shared = require('../shared');
|
||||
|
||||
/**
|
||||
* Generates a Transport object to generate JSON output
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} optional config parameter
|
||||
*/
|
||||
class JSONTransport {
|
||||
constructor(options) {
|
||||
options = options || {};
|
||||
|
||||
this.options = options || {};
|
||||
|
||||
this.name = 'JSONTransport';
|
||||
this.version = packageData.version;
|
||||
|
||||
this.logger = shared.getLogger(this.options, {
|
||||
component: this.options.component || 'json-transport'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Compiles a mailcomposer message and forwards it to handler that sends it.</p>
|
||||
*
|
||||
* @param {Object} emailMessage MailComposer object
|
||||
* @param {Function} callback Callback function to run when the sending is completed
|
||||
*/
|
||||
send(mail, done) {
|
||||
// Sendmail strips this header line by itself
|
||||
mail.message.keepBcc = true;
|
||||
|
||||
let envelope = mail.data.envelope || mail.message.getEnvelope();
|
||||
let messageId = mail.message.messageId();
|
||||
|
||||
let recipients = [].concat(envelope.to || []);
|
||||
if (recipients.length > 3) {
|
||||
recipients.push('...and ' + recipients.splice(2).length + ' more');
|
||||
}
|
||||
this.logger.info(
|
||||
{
|
||||
tnx: 'send',
|
||||
messageId
|
||||
},
|
||||
'Composing JSON structure of %s to <%s>',
|
||||
messageId,
|
||||
recipients.join(', ')
|
||||
);
|
||||
|
||||
setImmediate(() => {
|
||||
mail.normalize((err, data) => {
|
||||
if (err) {
|
||||
this.logger.error(
|
||||
{
|
||||
err,
|
||||
tnx: 'send',
|
||||
messageId
|
||||
},
|
||||
'Failed building JSON structure for %s. %s',
|
||||
messageId,
|
||||
err.message
|
||||
);
|
||||
return done(err);
|
||||
}
|
||||
|
||||
delete data.envelope;
|
||||
delete data.normalizedHeaders;
|
||||
|
||||
return done(null, {
|
||||
envelope,
|
||||
messageId,
|
||||
message: this.options.skipEncoding ? data : JSON.stringify(data)
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = JSONTransport;
|
||||
629
backend/node_modules/imapflow/node_modules/nodemailer/lib/mail-composer/index.js
generated
vendored
Normal file
629
backend/node_modules/imapflow/node_modules/nodemailer/lib/mail-composer/index.js
generated
vendored
Normal file
@@ -0,0 +1,629 @@
|
||||
/* eslint no-undefined: 0 */
|
||||
|
||||
'use strict';
|
||||
|
||||
const MimeNode = require('../mime-node');
|
||||
const mimeFuncs = require('../mime-funcs');
|
||||
const parseDataURI = require('../shared').parseDataURI;
|
||||
|
||||
/**
|
||||
* Creates the object for composing a MimeNode instance out from the mail options
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} mail Mail options
|
||||
*/
|
||||
class MailComposer {
|
||||
constructor(mail) {
|
||||
this.mail = mail || {};
|
||||
this.message = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds MimeNode instance
|
||||
*/
|
||||
compile() {
|
||||
this._alternatives = this.getAlternatives();
|
||||
this._htmlNode = this._alternatives.filter(alternative => /^text\/html\b/i.test(alternative.contentType)).pop();
|
||||
this._attachments = this.getAttachments(!!this._htmlNode);
|
||||
|
||||
this._useRelated = !!(this._htmlNode && this._attachments.related.length);
|
||||
this._useAlternative = this._alternatives.length > 1;
|
||||
this._useMixed = this._attachments.attached.length > 1 || (this._alternatives.length && this._attachments.attached.length === 1);
|
||||
|
||||
// Compose MIME tree
|
||||
if (this.mail.raw) {
|
||||
this.message = new MimeNode('message/rfc822', { newline: this.mail.newline }).setRaw(this.mail.raw);
|
||||
} else if (this._useMixed) {
|
||||
this.message = this._createMixed();
|
||||
} else if (this._useAlternative) {
|
||||
this.message = this._createAlternative();
|
||||
} else if (this._useRelated) {
|
||||
this.message = this._createRelated();
|
||||
} else {
|
||||
this.message = this._createContentNode(
|
||||
false,
|
||||
[]
|
||||
.concat(this._alternatives || [])
|
||||
.concat(this._attachments.attached || [])
|
||||
.shift() || {
|
||||
contentType: 'text/plain',
|
||||
content: ''
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Add custom headers
|
||||
if (this.mail.headers) {
|
||||
this.message.addHeader(this.mail.headers);
|
||||
}
|
||||
|
||||
// Add headers to the root node, always overrides custom headers
|
||||
['from', 'sender', 'to', 'cc', 'bcc', 'reply-to', 'in-reply-to', 'references', 'subject', 'message-id', 'date'].forEach(header => {
|
||||
let key = header.replace(/-(\w)/g, (o, c) => c.toUpperCase());
|
||||
if (this.mail[key]) {
|
||||
this.message.setHeader(header, this.mail[key]);
|
||||
}
|
||||
});
|
||||
|
||||
// Sets custom envelope
|
||||
if (this.mail.envelope) {
|
||||
this.message.setEnvelope(this.mail.envelope);
|
||||
}
|
||||
|
||||
// ensure Message-Id value
|
||||
this.message.messageId();
|
||||
|
||||
return this.message;
|
||||
}
|
||||
|
||||
/**
|
||||
* List all attachments. Resulting attachment objects can be used as input for MimeNode nodes
|
||||
*
|
||||
* @param {Boolean} findRelated If true separate related attachments from attached ones
|
||||
* @returns {Object} An object of arrays (`related` and `attached`)
|
||||
*/
|
||||
getAttachments(findRelated) {
|
||||
let icalEvent, eventObject;
|
||||
let attachments = [].concat(this.mail.attachments || []).map((attachment, i) => {
|
||||
let data;
|
||||
|
||||
if (/^data:/i.test(attachment.path || attachment.href)) {
|
||||
attachment = this._processDataUrl(attachment);
|
||||
}
|
||||
|
||||
let contentType =
|
||||
attachment.contentType || mimeFuncs.detectMimeType(attachment.filename || attachment.path || attachment.href || 'bin');
|
||||
|
||||
let isImage = /^image\//i.test(contentType);
|
||||
let isMessageNode = /^message\//i.test(contentType);
|
||||
|
||||
let contentDisposition =
|
||||
attachment.contentDisposition || (isMessageNode || (isImage && attachment.cid) ? 'inline' : 'attachment');
|
||||
|
||||
let contentTransferEncoding;
|
||||
if ('contentTransferEncoding' in attachment) {
|
||||
// also contains `false`, to set
|
||||
contentTransferEncoding = attachment.contentTransferEncoding;
|
||||
} else if (isMessageNode) {
|
||||
// the content might include non-ASCII bytes but at this point we do not know it yet
|
||||
contentTransferEncoding = '8bit';
|
||||
} else {
|
||||
contentTransferEncoding = 'base64'; // the default
|
||||
}
|
||||
|
||||
data = {
|
||||
contentType,
|
||||
contentDisposition,
|
||||
contentTransferEncoding
|
||||
};
|
||||
|
||||
if (attachment.filename) {
|
||||
data.filename = attachment.filename;
|
||||
} else if (!isMessageNode && attachment.filename !== false) {
|
||||
data.filename = (attachment.path || attachment.href || '').split('/').pop().split('?').shift() || 'attachment-' + (i + 1);
|
||||
if (data.filename.indexOf('.') < 0) {
|
||||
data.filename += '.' + mimeFuncs.detectExtension(data.contentType);
|
||||
}
|
||||
}
|
||||
|
||||
if (/^https?:\/\//i.test(attachment.path)) {
|
||||
attachment.href = attachment.path;
|
||||
attachment.path = undefined;
|
||||
}
|
||||
|
||||
if (attachment.cid) {
|
||||
data.cid = attachment.cid;
|
||||
}
|
||||
|
||||
if (attachment.raw) {
|
||||
data.raw = attachment.raw;
|
||||
} else if (attachment.path) {
|
||||
data.content = {
|
||||
path: attachment.path
|
||||
};
|
||||
} else if (attachment.href) {
|
||||
data.content = {
|
||||
href: attachment.href,
|
||||
httpHeaders: attachment.httpHeaders
|
||||
};
|
||||
} else {
|
||||
data.content = attachment.content || '';
|
||||
}
|
||||
|
||||
if (attachment.encoding) {
|
||||
data.encoding = attachment.encoding;
|
||||
}
|
||||
|
||||
if (attachment.headers) {
|
||||
data.headers = attachment.headers;
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
|
||||
if (this.mail.icalEvent) {
|
||||
if (
|
||||
typeof this.mail.icalEvent === 'object' &&
|
||||
(this.mail.icalEvent.content || this.mail.icalEvent.path || this.mail.icalEvent.href || this.mail.icalEvent.raw)
|
||||
) {
|
||||
icalEvent = this.mail.icalEvent;
|
||||
} else {
|
||||
icalEvent = {
|
||||
content: this.mail.icalEvent
|
||||
};
|
||||
}
|
||||
|
||||
eventObject = {};
|
||||
Object.keys(icalEvent).forEach(key => {
|
||||
eventObject[key] = icalEvent[key];
|
||||
});
|
||||
|
||||
eventObject.contentType = 'application/ics';
|
||||
if (!eventObject.headers) {
|
||||
eventObject.headers = {};
|
||||
}
|
||||
eventObject.filename = eventObject.filename || 'invite.ics';
|
||||
eventObject.headers['Content-Disposition'] = 'attachment';
|
||||
eventObject.headers['Content-Transfer-Encoding'] = 'base64';
|
||||
}
|
||||
|
||||
if (!findRelated) {
|
||||
return {
|
||||
attached: attachments.concat(eventObject || []),
|
||||
related: []
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
attached: attachments.filter(attachment => !attachment.cid).concat(eventObject || []),
|
||||
related: attachments.filter(attachment => !!attachment.cid)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List alternatives. Resulting objects can be used as input for MimeNode nodes
|
||||
*
|
||||
* @returns {Array} An array of alternative elements. Includes the `text` and `html` values as well
|
||||
*/
|
||||
getAlternatives() {
|
||||
let alternatives = [],
|
||||
text,
|
||||
html,
|
||||
watchHtml,
|
||||
amp,
|
||||
icalEvent,
|
||||
eventObject;
|
||||
|
||||
if (this.mail.text) {
|
||||
if (
|
||||
typeof this.mail.text === 'object' &&
|
||||
(this.mail.text.content || this.mail.text.path || this.mail.text.href || this.mail.text.raw)
|
||||
) {
|
||||
text = this.mail.text;
|
||||
} else {
|
||||
text = {
|
||||
content: this.mail.text
|
||||
};
|
||||
}
|
||||
text.contentType = 'text/plain; charset=utf-8';
|
||||
}
|
||||
|
||||
if (this.mail.watchHtml) {
|
||||
if (
|
||||
typeof this.mail.watchHtml === 'object' &&
|
||||
(this.mail.watchHtml.content || this.mail.watchHtml.path || this.mail.watchHtml.href || this.mail.watchHtml.raw)
|
||||
) {
|
||||
watchHtml = this.mail.watchHtml;
|
||||
} else {
|
||||
watchHtml = {
|
||||
content: this.mail.watchHtml
|
||||
};
|
||||
}
|
||||
watchHtml.contentType = 'text/watch-html; charset=utf-8';
|
||||
}
|
||||
|
||||
if (this.mail.amp) {
|
||||
if (
|
||||
typeof this.mail.amp === 'object' &&
|
||||
(this.mail.amp.content || this.mail.amp.path || this.mail.amp.href || this.mail.amp.raw)
|
||||
) {
|
||||
amp = this.mail.amp;
|
||||
} else {
|
||||
amp = {
|
||||
content: this.mail.amp
|
||||
};
|
||||
}
|
||||
amp.contentType = 'text/x-amp-html; charset=utf-8';
|
||||
}
|
||||
|
||||
// NB! when including attachments with a calendar alternative you might end up in a blank screen on some clients
|
||||
if (this.mail.icalEvent) {
|
||||
if (
|
||||
typeof this.mail.icalEvent === 'object' &&
|
||||
(this.mail.icalEvent.content || this.mail.icalEvent.path || this.mail.icalEvent.href || this.mail.icalEvent.raw)
|
||||
) {
|
||||
icalEvent = this.mail.icalEvent;
|
||||
} else {
|
||||
icalEvent = {
|
||||
content: this.mail.icalEvent
|
||||
};
|
||||
}
|
||||
|
||||
eventObject = {};
|
||||
Object.keys(icalEvent).forEach(key => {
|
||||
eventObject[key] = icalEvent[key];
|
||||
});
|
||||
|
||||
if (eventObject.content && typeof eventObject.content === 'object') {
|
||||
// we are going to have the same attachment twice, so mark this to be
|
||||
// resolved just once
|
||||
eventObject.content._resolve = true;
|
||||
}
|
||||
|
||||
eventObject.filename = false;
|
||||
eventObject.contentType =
|
||||
'text/calendar; charset=utf-8; method=' + (eventObject.method || 'PUBLISH').toString().trim().toUpperCase();
|
||||
if (!eventObject.headers) {
|
||||
eventObject.headers = {};
|
||||
}
|
||||
}
|
||||
|
||||
if (this.mail.html) {
|
||||
if (
|
||||
typeof this.mail.html === 'object' &&
|
||||
(this.mail.html.content || this.mail.html.path || this.mail.html.href || this.mail.html.raw)
|
||||
) {
|
||||
html = this.mail.html;
|
||||
} else {
|
||||
html = {
|
||||
content: this.mail.html
|
||||
};
|
||||
}
|
||||
html.contentType = 'text/html; charset=utf-8';
|
||||
}
|
||||
|
||||
[]
|
||||
.concat(text || [])
|
||||
.concat(watchHtml || [])
|
||||
.concat(amp || [])
|
||||
.concat(html || [])
|
||||
.concat(eventObject || [])
|
||||
.concat(this.mail.alternatives || [])
|
||||
.forEach(alternative => {
|
||||
let data;
|
||||
|
||||
if (/^data:/i.test(alternative.path || alternative.href)) {
|
||||
alternative = this._processDataUrl(alternative);
|
||||
}
|
||||
|
||||
data = {
|
||||
contentType:
|
||||
alternative.contentType ||
|
||||
mimeFuncs.detectMimeType(alternative.filename || alternative.path || alternative.href || 'txt'),
|
||||
contentTransferEncoding: alternative.contentTransferEncoding
|
||||
};
|
||||
|
||||
if (alternative.filename) {
|
||||
data.filename = alternative.filename;
|
||||
}
|
||||
|
||||
if (/^https?:\/\//i.test(alternative.path)) {
|
||||
alternative.href = alternative.path;
|
||||
alternative.path = undefined;
|
||||
}
|
||||
|
||||
if (alternative.raw) {
|
||||
data.raw = alternative.raw;
|
||||
} else if (alternative.path) {
|
||||
data.content = {
|
||||
path: alternative.path
|
||||
};
|
||||
} else if (alternative.href) {
|
||||
data.content = {
|
||||
href: alternative.href
|
||||
};
|
||||
} else {
|
||||
data.content = alternative.content || '';
|
||||
}
|
||||
|
||||
if (alternative.encoding) {
|
||||
data.encoding = alternative.encoding;
|
||||
}
|
||||
|
||||
if (alternative.headers) {
|
||||
data.headers = alternative.headers;
|
||||
}
|
||||
|
||||
alternatives.push(data);
|
||||
});
|
||||
|
||||
return alternatives;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds multipart/mixed node. It should always contain different type of elements on the same level
|
||||
* eg. text + attachments
|
||||
*
|
||||
* @param {Object} parentNode Parent for this note. If it does not exist, a root node is created
|
||||
* @returns {Object} MimeNode node element
|
||||
*/
|
||||
_createMixed(parentNode) {
|
||||
let node;
|
||||
|
||||
if (!parentNode) {
|
||||
node = new MimeNode('multipart/mixed', {
|
||||
baseBoundary: this.mail.baseBoundary,
|
||||
textEncoding: this.mail.textEncoding,
|
||||
boundaryPrefix: this.mail.boundaryPrefix,
|
||||
disableUrlAccess: this.mail.disableUrlAccess,
|
||||
disableFileAccess: this.mail.disableFileAccess,
|
||||
normalizeHeaderKey: this.mail.normalizeHeaderKey,
|
||||
newline: this.mail.newline
|
||||
});
|
||||
} else {
|
||||
node = parentNode.createChild('multipart/mixed', {
|
||||
disableUrlAccess: this.mail.disableUrlAccess,
|
||||
disableFileAccess: this.mail.disableFileAccess,
|
||||
normalizeHeaderKey: this.mail.normalizeHeaderKey,
|
||||
newline: this.mail.newline
|
||||
});
|
||||
}
|
||||
|
||||
if (this._useAlternative) {
|
||||
this._createAlternative(node);
|
||||
} else if (this._useRelated) {
|
||||
this._createRelated(node);
|
||||
}
|
||||
|
||||
[]
|
||||
.concat((!this._useAlternative && this._alternatives) || [])
|
||||
.concat(this._attachments.attached || [])
|
||||
.forEach(element => {
|
||||
// if the element is a html node from related subpart then ignore it
|
||||
if (!this._useRelated || element !== this._htmlNode) {
|
||||
this._createContentNode(node, element);
|
||||
}
|
||||
});
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds multipart/alternative node. It should always contain same type of elements on the same level
|
||||
* eg. text + html view of the same data
|
||||
*
|
||||
* @param {Object} parentNode Parent for this note. If it does not exist, a root node is created
|
||||
* @returns {Object} MimeNode node element
|
||||
*/
|
||||
_createAlternative(parentNode) {
|
||||
let node;
|
||||
|
||||
if (!parentNode) {
|
||||
node = new MimeNode('multipart/alternative', {
|
||||
baseBoundary: this.mail.baseBoundary,
|
||||
textEncoding: this.mail.textEncoding,
|
||||
boundaryPrefix: this.mail.boundaryPrefix,
|
||||
disableUrlAccess: this.mail.disableUrlAccess,
|
||||
disableFileAccess: this.mail.disableFileAccess,
|
||||
normalizeHeaderKey: this.mail.normalizeHeaderKey,
|
||||
newline: this.mail.newline
|
||||
});
|
||||
} else {
|
||||
node = parentNode.createChild('multipart/alternative', {
|
||||
disableUrlAccess: this.mail.disableUrlAccess,
|
||||
disableFileAccess: this.mail.disableFileAccess,
|
||||
normalizeHeaderKey: this.mail.normalizeHeaderKey,
|
||||
newline: this.mail.newline
|
||||
});
|
||||
}
|
||||
|
||||
this._alternatives.forEach(alternative => {
|
||||
if (this._useRelated && this._htmlNode === alternative) {
|
||||
this._createRelated(node);
|
||||
} else {
|
||||
this._createContentNode(node, alternative);
|
||||
}
|
||||
});
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds multipart/related node. It should always contain html node with related attachments
|
||||
*
|
||||
* @param {Object} parentNode Parent for this note. If it does not exist, a root node is created
|
||||
* @returns {Object} MimeNode node element
|
||||
*/
|
||||
_createRelated(parentNode) {
|
||||
let node;
|
||||
|
||||
if (!parentNode) {
|
||||
node = new MimeNode('multipart/related; type="text/html"', {
|
||||
baseBoundary: this.mail.baseBoundary,
|
||||
textEncoding: this.mail.textEncoding,
|
||||
boundaryPrefix: this.mail.boundaryPrefix,
|
||||
disableUrlAccess: this.mail.disableUrlAccess,
|
||||
disableFileAccess: this.mail.disableFileAccess,
|
||||
normalizeHeaderKey: this.mail.normalizeHeaderKey,
|
||||
newline: this.mail.newline
|
||||
});
|
||||
} else {
|
||||
node = parentNode.createChild('multipart/related; type="text/html"', {
|
||||
disableUrlAccess: this.mail.disableUrlAccess,
|
||||
disableFileAccess: this.mail.disableFileAccess,
|
||||
normalizeHeaderKey: this.mail.normalizeHeaderKey,
|
||||
newline: this.mail.newline
|
||||
});
|
||||
}
|
||||
|
||||
this._createContentNode(node, this._htmlNode);
|
||||
|
||||
this._attachments.related.forEach(alternative => this._createContentNode(node, alternative));
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a regular node with contents
|
||||
*
|
||||
* @param {Object} parentNode Parent for this note. If it does not exist, a root node is created
|
||||
* @param {Object} element Node data
|
||||
* @returns {Object} MimeNode node element
|
||||
*/
|
||||
_createContentNode(parentNode, element) {
|
||||
element = element || {};
|
||||
element.content = element.content || '';
|
||||
|
||||
let node;
|
||||
let encoding = (element.encoding || 'utf8')
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.replace(/[-_\s]/g, '');
|
||||
|
||||
if (!parentNode) {
|
||||
node = new MimeNode(element.contentType, {
|
||||
filename: element.filename,
|
||||
baseBoundary: this.mail.baseBoundary,
|
||||
textEncoding: this.mail.textEncoding,
|
||||
boundaryPrefix: this.mail.boundaryPrefix,
|
||||
disableUrlAccess: this.mail.disableUrlAccess,
|
||||
disableFileAccess: this.mail.disableFileAccess,
|
||||
normalizeHeaderKey: this.mail.normalizeHeaderKey,
|
||||
newline: this.mail.newline
|
||||
});
|
||||
} else {
|
||||
node = parentNode.createChild(element.contentType, {
|
||||
filename: element.filename,
|
||||
textEncoding: this.mail.textEncoding,
|
||||
disableUrlAccess: this.mail.disableUrlAccess,
|
||||
disableFileAccess: this.mail.disableFileAccess,
|
||||
normalizeHeaderKey: this.mail.normalizeHeaderKey,
|
||||
newline: this.mail.newline
|
||||
});
|
||||
}
|
||||
|
||||
// add custom headers
|
||||
if (element.headers) {
|
||||
node.addHeader(element.headers);
|
||||
}
|
||||
|
||||
if (element.cid) {
|
||||
node.setHeader('Content-Id', '<' + element.cid.replace(/[<>]/g, '') + '>');
|
||||
}
|
||||
|
||||
if (element.contentTransferEncoding) {
|
||||
node.setHeader('Content-Transfer-Encoding', element.contentTransferEncoding);
|
||||
} else if (this.mail.encoding && /^text\//i.test(element.contentType)) {
|
||||
node.setHeader('Content-Transfer-Encoding', this.mail.encoding);
|
||||
}
|
||||
|
||||
if (!/^text\//i.test(element.contentType) || element.contentDisposition) {
|
||||
node.setHeader(
|
||||
'Content-Disposition',
|
||||
element.contentDisposition || (element.cid && /^image\//i.test(element.contentType) ? 'inline' : 'attachment')
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof element.content === 'string' && !['utf8', 'usascii', 'ascii'].includes(encoding)) {
|
||||
element.content = Buffer.from(element.content, encoding);
|
||||
}
|
||||
|
||||
// prefer pregenerated raw content
|
||||
if (element.raw) {
|
||||
node.setRaw(element.raw);
|
||||
} else {
|
||||
node.setContent(element.content);
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses data uri and converts it to a Buffer
|
||||
*
|
||||
* @param {Object} element Content element
|
||||
* @return {Object} Parsed element
|
||||
*/
|
||||
_processDataUrl(element) {
|
||||
const dataUrl = element.path || element.href;
|
||||
|
||||
// Early validation to prevent ReDoS
|
||||
if (!dataUrl || typeof dataUrl !== 'string') {
|
||||
return element;
|
||||
}
|
||||
|
||||
if (!dataUrl.startsWith('data:')) {
|
||||
return element;
|
||||
}
|
||||
|
||||
if (dataUrl.length > 52428800) {
|
||||
// 52428800 chars = 50MB limit for data URL string (~37.5MB decoded image)
|
||||
// Extract content type before rejecting to preserve MIME type
|
||||
let detectedType = 'application/octet-stream';
|
||||
const commaPos = dataUrl.indexOf(',');
|
||||
|
||||
if (commaPos > 0 && commaPos < 200) {
|
||||
// Parse header safely with size limit
|
||||
const header = dataUrl.substring(5, commaPos); // skip 'data:'
|
||||
const parts = header.split(';');
|
||||
if (parts[0] && parts[0].includes('/')) {
|
||||
detectedType = parts[0].trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Return empty content for excessively long data URLs
|
||||
return Object.assign({}, element, {
|
||||
path: false,
|
||||
href: false,
|
||||
content: Buffer.alloc(0),
|
||||
contentType: element.contentType || detectedType
|
||||
});
|
||||
}
|
||||
|
||||
let parsedDataUri;
|
||||
try {
|
||||
parsedDataUri = parseDataURI(dataUrl);
|
||||
} catch (_err) {
|
||||
return element;
|
||||
}
|
||||
|
||||
if (!parsedDataUri) {
|
||||
return element;
|
||||
}
|
||||
|
||||
element.content = parsedDataUri.data;
|
||||
element.contentType = element.contentType || parsedDataUri.contentType;
|
||||
|
||||
if ('path' in element) {
|
||||
element.path = false;
|
||||
}
|
||||
|
||||
if ('href' in element) {
|
||||
element.href = false;
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MailComposer;
|
||||
441
backend/node_modules/imapflow/node_modules/nodemailer/lib/mailer/index.js
generated
vendored
Normal file
441
backend/node_modules/imapflow/node_modules/nodemailer/lib/mailer/index.js
generated
vendored
Normal file
@@ -0,0 +1,441 @@
|
||||
'use strict';
|
||||
|
||||
const EventEmitter = require('events');
|
||||
const shared = require('../shared');
|
||||
const mimeTypes = require('../mime-funcs/mime-types');
|
||||
const MailComposer = require('../mail-composer');
|
||||
const DKIM = require('../dkim');
|
||||
const httpProxyClient = require('../smtp-connection/http-proxy-client');
|
||||
const util = require('util');
|
||||
const urllib = require('url');
|
||||
const packageData = require('../../package.json');
|
||||
const MailMessage = require('./mail-message');
|
||||
const net = require('net');
|
||||
const dns = require('dns');
|
||||
const crypto = require('crypto');
|
||||
|
||||
/**
|
||||
* Creates an object for exposing the Mail API
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} transporter Transport object instance to pass the mails to
|
||||
*/
|
||||
class Mail extends EventEmitter {
|
||||
constructor(transporter, options, defaults) {
|
||||
super();
|
||||
|
||||
this.options = options || {};
|
||||
this._defaults = defaults || {};
|
||||
|
||||
this._defaultPlugins = {
|
||||
compile: [(...args) => this._convertDataImages(...args)],
|
||||
stream: []
|
||||
};
|
||||
|
||||
this._userPlugins = {
|
||||
compile: [],
|
||||
stream: []
|
||||
};
|
||||
|
||||
this.meta = new Map();
|
||||
|
||||
this.dkim = this.options.dkim ? new DKIM(this.options.dkim) : false;
|
||||
|
||||
this.transporter = transporter;
|
||||
this.transporter.mailer = this;
|
||||
|
||||
this.logger = shared.getLogger(this.options, {
|
||||
component: this.options.component || 'mail'
|
||||
});
|
||||
|
||||
this.logger.debug(
|
||||
{
|
||||
tnx: 'create'
|
||||
},
|
||||
'Creating transport: %s',
|
||||
this.getVersionString()
|
||||
);
|
||||
|
||||
// setup emit handlers for the transporter
|
||||
if (typeof this.transporter.on === 'function') {
|
||||
// deprecated log interface
|
||||
this.transporter.on('log', log => {
|
||||
this.logger.debug(
|
||||
{
|
||||
tnx: 'transport'
|
||||
},
|
||||
'%s: %s',
|
||||
log.type,
|
||||
log.message
|
||||
);
|
||||
});
|
||||
|
||||
// transporter errors
|
||||
this.transporter.on('error', err => {
|
||||
this.logger.error(
|
||||
{
|
||||
err,
|
||||
tnx: 'transport'
|
||||
},
|
||||
'Transport Error: %s',
|
||||
err.message
|
||||
);
|
||||
this.emit('error', err);
|
||||
});
|
||||
|
||||
// indicates if the sender has became idle
|
||||
this.transporter.on('idle', (...args) => {
|
||||
this.emit('idle', ...args);
|
||||
});
|
||||
|
||||
// indicates if the sender has became idle and all connections are terminated
|
||||
this.transporter.on('clear', (...args) => {
|
||||
this.emit('clear', ...args);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional methods passed to the underlying transport object
|
||||
*/
|
||||
['close', 'isIdle', 'verify'].forEach(method => {
|
||||
this[method] = (...args) => {
|
||||
if (typeof this.transporter[method] === 'function') {
|
||||
if (method === 'verify' && typeof this.getSocket === 'function') {
|
||||
this.transporter.getSocket = this.getSocket;
|
||||
this.getSocket = false;
|
||||
}
|
||||
return this.transporter[method](...args);
|
||||
} else {
|
||||
this.logger.warn(
|
||||
{
|
||||
tnx: 'transport',
|
||||
methodName: method
|
||||
},
|
||||
'Non existing method %s called for transport',
|
||||
method
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// setup proxy handling
|
||||
if (this.options.proxy && typeof this.options.proxy === 'string') {
|
||||
this.setupProxy(this.options.proxy);
|
||||
}
|
||||
}
|
||||
|
||||
use(step, plugin) {
|
||||
step = (step || '').toString();
|
||||
if (!this._userPlugins.hasOwnProperty(step)) {
|
||||
this._userPlugins[step] = [plugin];
|
||||
} else {
|
||||
this._userPlugins[step].push(plugin);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an email using the preselected transport object
|
||||
*
|
||||
* @param {Object} data E-data description
|
||||
* @param {Function?} callback Callback to run once the sending succeeded or failed
|
||||
*/
|
||||
sendMail(data, callback = null) {
|
||||
let promise;
|
||||
|
||||
if (!callback) {
|
||||
promise = new Promise((resolve, reject) => {
|
||||
callback = shared.callbackPromise(resolve, reject);
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof this.getSocket === 'function') {
|
||||
this.transporter.getSocket = this.getSocket;
|
||||
this.getSocket = false;
|
||||
}
|
||||
|
||||
let mail = new MailMessage(this, data);
|
||||
|
||||
this.logger.debug(
|
||||
{
|
||||
tnx: 'transport',
|
||||
name: this.transporter.name,
|
||||
version: this.transporter.version,
|
||||
action: 'send'
|
||||
},
|
||||
'Sending mail using %s/%s',
|
||||
this.transporter.name,
|
||||
this.transporter.version
|
||||
);
|
||||
|
||||
this._processPlugins('compile', mail, err => {
|
||||
if (err) {
|
||||
this.logger.error(
|
||||
{
|
||||
err,
|
||||
tnx: 'plugin',
|
||||
action: 'compile'
|
||||
},
|
||||
'PluginCompile Error: %s',
|
||||
err.message
|
||||
);
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
mail.message = new MailComposer(mail.data).compile();
|
||||
|
||||
mail.setMailerHeader();
|
||||
mail.setPriorityHeaders();
|
||||
mail.setListHeaders();
|
||||
|
||||
this._processPlugins('stream', mail, err => {
|
||||
if (err) {
|
||||
this.logger.error(
|
||||
{
|
||||
err,
|
||||
tnx: 'plugin',
|
||||
action: 'stream'
|
||||
},
|
||||
'PluginStream Error: %s',
|
||||
err.message
|
||||
);
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (mail.data.dkim || this.dkim) {
|
||||
mail.message.processFunc(input => {
|
||||
let dkim = mail.data.dkim ? new DKIM(mail.data.dkim) : this.dkim;
|
||||
this.logger.debug(
|
||||
{
|
||||
tnx: 'DKIM',
|
||||
messageId: mail.message.messageId(),
|
||||
dkimDomains: dkim.keys.map(key => key.keySelector + '.' + key.domainName).join(', ')
|
||||
},
|
||||
'Signing outgoing message with %s keys',
|
||||
dkim.keys.length
|
||||
);
|
||||
return dkim.sign(input, mail.data._dkim);
|
||||
});
|
||||
}
|
||||
|
||||
this.transporter.send(mail, (...args) => {
|
||||
if (args[0]) {
|
||||
this.logger.error(
|
||||
{
|
||||
err: args[0],
|
||||
tnx: 'transport',
|
||||
action: 'send'
|
||||
},
|
||||
'Send Error: %s',
|
||||
args[0].message
|
||||
);
|
||||
}
|
||||
callback(...args);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
getVersionString() {
|
||||
return util.format(
|
||||
'%s (%s; +%s; %s/%s)',
|
||||
packageData.name,
|
||||
packageData.version,
|
||||
packageData.homepage,
|
||||
this.transporter.name,
|
||||
this.transporter.version
|
||||
);
|
||||
}
|
||||
|
||||
_processPlugins(step, mail, callback) {
|
||||
step = (step || '').toString();
|
||||
|
||||
if (!this._userPlugins.hasOwnProperty(step)) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
let userPlugins = this._userPlugins[step] || [];
|
||||
let defaultPlugins = this._defaultPlugins[step] || [];
|
||||
|
||||
if (userPlugins.length) {
|
||||
this.logger.debug(
|
||||
{
|
||||
tnx: 'transaction',
|
||||
pluginCount: userPlugins.length,
|
||||
step
|
||||
},
|
||||
'Using %s plugins for %s',
|
||||
userPlugins.length,
|
||||
step
|
||||
);
|
||||
}
|
||||
|
||||
if (userPlugins.length + defaultPlugins.length === 0) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
let pos = 0;
|
||||
let block = 'default';
|
||||
let processPlugins = () => {
|
||||
let curplugins = block === 'default' ? defaultPlugins : userPlugins;
|
||||
if (pos >= curplugins.length) {
|
||||
if (block === 'default' && userPlugins.length) {
|
||||
block = 'user';
|
||||
pos = 0;
|
||||
curplugins = userPlugins;
|
||||
} else {
|
||||
return callback();
|
||||
}
|
||||
}
|
||||
let plugin = curplugins[pos++];
|
||||
plugin(mail, err => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
processPlugins();
|
||||
});
|
||||
};
|
||||
|
||||
processPlugins();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up proxy handler for a Nodemailer object
|
||||
*
|
||||
* @param {String} proxyUrl Proxy configuration url
|
||||
*/
|
||||
setupProxy(proxyUrl) {
|
||||
let proxy = urllib.parse(proxyUrl);
|
||||
|
||||
// setup socket handler for the mailer object
|
||||
this.getSocket = (options, callback) => {
|
||||
let protocol = proxy.protocol.replace(/:$/, '').toLowerCase();
|
||||
|
||||
if (this.meta.has('proxy_handler_' + protocol)) {
|
||||
return this.meta.get('proxy_handler_' + protocol)(proxy, options, callback);
|
||||
}
|
||||
|
||||
switch (protocol) {
|
||||
// Connect using a HTTP CONNECT method
|
||||
case 'http':
|
||||
case 'https':
|
||||
httpProxyClient(proxy.href, options.port, options.host, (err, socket) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, {
|
||||
connection: socket
|
||||
});
|
||||
});
|
||||
return;
|
||||
case 'socks':
|
||||
case 'socks5':
|
||||
case 'socks4':
|
||||
case 'socks4a': {
|
||||
if (!this.meta.has('proxy_socks_module')) {
|
||||
return callback(new Error('Socks module not loaded'));
|
||||
}
|
||||
let connect = ipaddress => {
|
||||
let proxyV2 = !!this.meta.get('proxy_socks_module').SocksClient;
|
||||
let socksClient = proxyV2 ? this.meta.get('proxy_socks_module').SocksClient : this.meta.get('proxy_socks_module');
|
||||
let proxyType = Number(proxy.protocol.replace(/\D/g, '')) || 5;
|
||||
let connectionOpts = {
|
||||
proxy: {
|
||||
ipaddress,
|
||||
port: Number(proxy.port),
|
||||
type: proxyType
|
||||
},
|
||||
[proxyV2 ? 'destination' : 'target']: {
|
||||
host: options.host,
|
||||
port: options.port
|
||||
},
|
||||
command: 'connect'
|
||||
};
|
||||
|
||||
if (proxy.auth) {
|
||||
let username = decodeURIComponent(proxy.auth.split(':').shift());
|
||||
let password = decodeURIComponent(proxy.auth.split(':').pop());
|
||||
if (proxyV2) {
|
||||
connectionOpts.proxy.userId = username;
|
||||
connectionOpts.proxy.password = password;
|
||||
} else if (proxyType === 4) {
|
||||
connectionOpts.userid = username;
|
||||
} else {
|
||||
connectionOpts.authentication = {
|
||||
username,
|
||||
password
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
socksClient.createConnection(connectionOpts, (err, info) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, {
|
||||
connection: info.socket || info
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (net.isIP(proxy.hostname)) {
|
||||
return connect(proxy.hostname);
|
||||
}
|
||||
|
||||
return dns.resolve(proxy.hostname, (err, address) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
connect(Array.isArray(address) ? address[0] : address);
|
||||
});
|
||||
}
|
||||
}
|
||||
callback(new Error('Unknown proxy configuration'));
|
||||
};
|
||||
}
|
||||
|
||||
_convertDataImages(mail, callback) {
|
||||
if ((!this.options.attachDataUrls && !mail.data.attachDataUrls) || !mail.data.html) {
|
||||
return callback();
|
||||
}
|
||||
mail.resolveContent(mail.data, 'html', (err, html) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
let cidCounter = 0;
|
||||
html = (html || '')
|
||||
.toString()
|
||||
.replace(/(<img\b[^<>]{0,1024} src\s{0,20}=[\s"']{0,20})(data:([^;]+);[^"'>\s]+)/gi, (match, prefix, dataUri, mimeType) => {
|
||||
let cid = crypto.randomBytes(10).toString('hex') + '@localhost';
|
||||
if (!mail.data.attachments) {
|
||||
mail.data.attachments = [];
|
||||
}
|
||||
if (!Array.isArray(mail.data.attachments)) {
|
||||
mail.data.attachments = [].concat(mail.data.attachments || []);
|
||||
}
|
||||
mail.data.attachments.push({
|
||||
path: dataUri,
|
||||
cid,
|
||||
filename: 'image-' + ++cidCounter + '.' + mimeTypes.detectExtension(mimeType)
|
||||
});
|
||||
return prefix + 'cid:' + cid;
|
||||
});
|
||||
mail.data.html = html;
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
set(key, value) {
|
||||
return this.meta.set(key, value);
|
||||
}
|
||||
|
||||
get(key) {
|
||||
return this.meta.get(key);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Mail;
|
||||
316
backend/node_modules/imapflow/node_modules/nodemailer/lib/mailer/mail-message.js
generated
vendored
Normal file
316
backend/node_modules/imapflow/node_modules/nodemailer/lib/mailer/mail-message.js
generated
vendored
Normal file
@@ -0,0 +1,316 @@
|
||||
'use strict';
|
||||
|
||||
const shared = require('../shared');
|
||||
const MimeNode = require('../mime-node');
|
||||
const mimeFuncs = require('../mime-funcs');
|
||||
|
||||
class MailMessage {
|
||||
constructor(mailer, data) {
|
||||
this.mailer = mailer;
|
||||
this.data = {};
|
||||
this.message = null;
|
||||
|
||||
data = data || {};
|
||||
let options = mailer.options || {};
|
||||
let defaults = mailer._defaults || {};
|
||||
|
||||
Object.keys(data).forEach(key => {
|
||||
this.data[key] = data[key];
|
||||
});
|
||||
|
||||
this.data.headers = this.data.headers || {};
|
||||
|
||||
// apply defaults
|
||||
Object.keys(defaults).forEach(key => {
|
||||
if (!(key in this.data)) {
|
||||
this.data[key] = defaults[key];
|
||||
} else if (key === 'headers') {
|
||||
// headers is a special case. Allow setting individual default headers
|
||||
Object.keys(defaults.headers).forEach(key => {
|
||||
if (!(key in this.data.headers)) {
|
||||
this.data.headers[key] = defaults.headers[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// force specific keys from transporter options
|
||||
['disableFileAccess', 'disableUrlAccess', 'normalizeHeaderKey'].forEach(key => {
|
||||
if (key in options) {
|
||||
this.data[key] = options[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
resolveContent(...args) {
|
||||
return shared.resolveContent(...args);
|
||||
}
|
||||
|
||||
resolveAll(callback) {
|
||||
let keys = [
|
||||
[this.data, 'html'],
|
||||
[this.data, 'text'],
|
||||
[this.data, 'watchHtml'],
|
||||
[this.data, 'amp'],
|
||||
[this.data, 'icalEvent']
|
||||
];
|
||||
|
||||
if (this.data.alternatives && this.data.alternatives.length) {
|
||||
this.data.alternatives.forEach((alternative, i) => {
|
||||
keys.push([this.data.alternatives, i]);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.data.attachments && this.data.attachments.length) {
|
||||
this.data.attachments.forEach((attachment, i) => {
|
||||
if (!attachment.filename) {
|
||||
attachment.filename =
|
||||
(attachment.path || attachment.href || '').split('/').pop().split('?').shift() || 'attachment-' + (i + 1);
|
||||
if (attachment.filename.indexOf('.') < 0) {
|
||||
attachment.filename += '.' + mimeFuncs.detectExtension(attachment.contentType);
|
||||
}
|
||||
}
|
||||
|
||||
if (!attachment.contentType) {
|
||||
attachment.contentType = mimeFuncs.detectMimeType(attachment.filename || attachment.path || attachment.href || 'bin');
|
||||
}
|
||||
|
||||
keys.push([this.data.attachments, i]);
|
||||
});
|
||||
}
|
||||
|
||||
let mimeNode = new MimeNode();
|
||||
|
||||
let addressKeys = ['from', 'to', 'cc', 'bcc', 'sender', 'replyTo'];
|
||||
|
||||
addressKeys.forEach(address => {
|
||||
let value;
|
||||
if (this.message) {
|
||||
value = [].concat(mimeNode._parseAddresses(this.message.getHeader(address === 'replyTo' ? 'reply-to' : address)) || []);
|
||||
} else if (this.data[address]) {
|
||||
value = [].concat(mimeNode._parseAddresses(this.data[address]) || []);
|
||||
}
|
||||
if (value && value.length) {
|
||||
this.data[address] = value;
|
||||
} else if (address in this.data) {
|
||||
this.data[address] = null;
|
||||
}
|
||||
});
|
||||
|
||||
let singleKeys = ['from', 'sender'];
|
||||
singleKeys.forEach(address => {
|
||||
if (this.data[address]) {
|
||||
this.data[address] = this.data[address].shift();
|
||||
}
|
||||
});
|
||||
|
||||
let pos = 0;
|
||||
let resolveNext = () => {
|
||||
if (pos >= keys.length) {
|
||||
return callback(null, this.data);
|
||||
}
|
||||
let args = keys[pos++];
|
||||
if (!args[0] || !args[0][args[1]]) {
|
||||
return resolveNext();
|
||||
}
|
||||
shared.resolveContent(...args, (err, value) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let node = {
|
||||
content: value
|
||||
};
|
||||
if (args[0][args[1]] && typeof args[0][args[1]] === 'object' && !Buffer.isBuffer(args[0][args[1]])) {
|
||||
Object.keys(args[0][args[1]]).forEach(key => {
|
||||
if (!(key in node) && !['content', 'path', 'href', 'raw'].includes(key)) {
|
||||
node[key] = args[0][args[1]][key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
args[0][args[1]] = node;
|
||||
resolveNext();
|
||||
});
|
||||
};
|
||||
|
||||
setImmediate(() => resolveNext());
|
||||
}
|
||||
|
||||
normalize(callback) {
|
||||
let envelope = this.data.envelope || this.message.getEnvelope();
|
||||
let messageId = this.message.messageId();
|
||||
|
||||
this.resolveAll((err, data) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
data.envelope = envelope;
|
||||
data.messageId = messageId;
|
||||
|
||||
['html', 'text', 'watchHtml', 'amp'].forEach(key => {
|
||||
if (data[key] && data[key].content) {
|
||||
if (typeof data[key].content === 'string') {
|
||||
data[key] = data[key].content;
|
||||
} else if (Buffer.isBuffer(data[key].content)) {
|
||||
data[key] = data[key].content.toString();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (data.icalEvent && Buffer.isBuffer(data.icalEvent.content)) {
|
||||
data.icalEvent.content = data.icalEvent.content.toString('base64');
|
||||
data.icalEvent.encoding = 'base64';
|
||||
}
|
||||
|
||||
if (data.alternatives && data.alternatives.length) {
|
||||
data.alternatives.forEach(alternative => {
|
||||
if (alternative && alternative.content && Buffer.isBuffer(alternative.content)) {
|
||||
alternative.content = alternative.content.toString('base64');
|
||||
alternative.encoding = 'base64';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (data.attachments && data.attachments.length) {
|
||||
data.attachments.forEach(attachment => {
|
||||
if (attachment && attachment.content && Buffer.isBuffer(attachment.content)) {
|
||||
attachment.content = attachment.content.toString('base64');
|
||||
attachment.encoding = 'base64';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
data.normalizedHeaders = {};
|
||||
Object.keys(data.headers || {}).forEach(key => {
|
||||
let value = [].concat(data.headers[key] || []).shift();
|
||||
value = (value && value.value) || value;
|
||||
if (value) {
|
||||
if (['references', 'in-reply-to', 'message-id', 'content-id'].includes(key)) {
|
||||
value = this.message._encodeHeaderValue(key, value);
|
||||
}
|
||||
data.normalizedHeaders[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
if (data.list && typeof data.list === 'object') {
|
||||
let listHeaders = this._getListHeaders(data.list);
|
||||
listHeaders.forEach(entry => {
|
||||
data.normalizedHeaders[entry.key] = entry.value.map(val => (val && val.value) || val).join(', ');
|
||||
});
|
||||
}
|
||||
|
||||
if (data.references) {
|
||||
data.normalizedHeaders.references = this.message._encodeHeaderValue('references', data.references);
|
||||
}
|
||||
|
||||
if (data.inReplyTo) {
|
||||
data.normalizedHeaders['in-reply-to'] = this.message._encodeHeaderValue('in-reply-to', data.inReplyTo);
|
||||
}
|
||||
|
||||
return callback(null, data);
|
||||
});
|
||||
}
|
||||
|
||||
setMailerHeader() {
|
||||
if (!this.message || !this.data.xMailer) {
|
||||
return;
|
||||
}
|
||||
this.message.setHeader('X-Mailer', this.data.xMailer);
|
||||
}
|
||||
|
||||
setPriorityHeaders() {
|
||||
if (!this.message || !this.data.priority) {
|
||||
return;
|
||||
}
|
||||
switch ((this.data.priority || '').toString().toLowerCase()) {
|
||||
case 'high':
|
||||
this.message.setHeader('X-Priority', '1 (Highest)');
|
||||
this.message.setHeader('X-MSMail-Priority', 'High');
|
||||
this.message.setHeader('Importance', 'High');
|
||||
break;
|
||||
case 'low':
|
||||
this.message.setHeader('X-Priority', '5 (Lowest)');
|
||||
this.message.setHeader('X-MSMail-Priority', 'Low');
|
||||
this.message.setHeader('Importance', 'Low');
|
||||
break;
|
||||
default:
|
||||
// do not add anything, since all messages are 'Normal' by default
|
||||
}
|
||||
}
|
||||
|
||||
setListHeaders() {
|
||||
if (!this.message || !this.data.list || typeof this.data.list !== 'object') {
|
||||
return;
|
||||
}
|
||||
// add optional List-* headers
|
||||
if (this.data.list && typeof this.data.list === 'object') {
|
||||
this._getListHeaders(this.data.list).forEach(listHeader => {
|
||||
listHeader.value.forEach(value => {
|
||||
this.message.addHeader(listHeader.key, value);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_getListHeaders(listData) {
|
||||
// make sure an url looks like <protocol:url>
|
||||
return Object.keys(listData).map(key => ({
|
||||
key: 'list-' + key.toLowerCase().trim(),
|
||||
value: [].concat(listData[key] || []).map(value => ({
|
||||
prepared: true,
|
||||
foldLines: true,
|
||||
value: []
|
||||
.concat(value || [])
|
||||
.map(value => {
|
||||
if (typeof value === 'string') {
|
||||
value = {
|
||||
url: value
|
||||
};
|
||||
}
|
||||
|
||||
if (value && value.url) {
|
||||
if (key.toLowerCase().trim() === 'id') {
|
||||
// List-ID: "comment" <domain>
|
||||
let comment = value.comment || '';
|
||||
if (mimeFuncs.isPlainText(comment)) {
|
||||
comment = '"' + comment + '"';
|
||||
} else {
|
||||
comment = mimeFuncs.encodeWord(comment);
|
||||
}
|
||||
|
||||
return (value.comment ? comment + ' ' : '') + this._formatListUrl(value.url).replace(/^<[^:]+\/{,2}/, '');
|
||||
}
|
||||
|
||||
// List-*: <http://domain> (comment)
|
||||
let comment = value.comment || '';
|
||||
if (!mimeFuncs.isPlainText(comment)) {
|
||||
comment = mimeFuncs.encodeWord(comment);
|
||||
}
|
||||
|
||||
return this._formatListUrl(value.url) + (value.comment ? ' (' + comment + ')' : '');
|
||||
}
|
||||
|
||||
return '';
|
||||
})
|
||||
.filter(value => value)
|
||||
.join(', ')
|
||||
}))
|
||||
}));
|
||||
}
|
||||
|
||||
_formatListUrl(url) {
|
||||
url = url.replace(/[\s<]+|[\s>]+/g, '');
|
||||
if (/^(https?|mailto|ftp):/.test(url)) {
|
||||
return '<' + url + '>';
|
||||
}
|
||||
if (/^[^@]+@[^@]+$/.test(url)) {
|
||||
return '<mailto:' + url + '>';
|
||||
}
|
||||
|
||||
return '<http://' + url + '>';
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MailMessage;
|
||||
625
backend/node_modules/imapflow/node_modules/nodemailer/lib/mime-funcs/index.js
generated
vendored
Normal file
625
backend/node_modules/imapflow/node_modules/nodemailer/lib/mime-funcs/index.js
generated
vendored
Normal file
@@ -0,0 +1,625 @@
|
||||
/* eslint no-control-regex:0 */
|
||||
|
||||
'use strict';
|
||||
|
||||
const base64 = require('../base64');
|
||||
const qp = require('../qp');
|
||||
const mimeTypes = require('./mime-types');
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* Checks if a value is plaintext string (uses only printable 7bit chars)
|
||||
*
|
||||
* @param {String} value String to be tested
|
||||
* @returns {Boolean} true if it is a plaintext string
|
||||
*/
|
||||
isPlainText(value, isParam) {
|
||||
const re = isParam ? /[\x00-\x08\x0b\x0c\x0e-\x1f"\u0080-\uFFFF]/ : /[\x00-\x08\x0b\x0c\x0e-\x1f\u0080-\uFFFF]/;
|
||||
if (typeof value !== 'string' || re.test(value)) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if a multi line string containes lines longer than the selected value.
|
||||
*
|
||||
* Useful when detecting if a mail message needs any processing at all –
|
||||
* if only plaintext characters are used and lines are short, then there is
|
||||
* no need to encode the values in any way. If the value is plaintext but has
|
||||
* longer lines then allowed, then use format=flowed
|
||||
*
|
||||
* @param {Number} lineLength Max line length to check for
|
||||
* @returns {Boolean} Returns true if there is at least one line longer than lineLength chars
|
||||
*/
|
||||
hasLongerLines(str, lineLength) {
|
||||
if (str.length > 128 * 1024) {
|
||||
// do not test strings longer than 128kB
|
||||
return true;
|
||||
}
|
||||
return new RegExp('^.{' + (lineLength + 1) + ',}', 'm').test(str);
|
||||
},
|
||||
|
||||
/**
|
||||
* Encodes a string or an Buffer to an UTF-8 MIME Word (rfc2047)
|
||||
*
|
||||
* @param {String|Buffer} data String to be encoded
|
||||
* @param {String} mimeWordEncoding='Q' Encoding for the mime word, either Q or B
|
||||
* @param {Number} [maxLength=0] If set, split mime words into several chunks if needed
|
||||
* @return {String} Single or several mime words joined together
|
||||
*/
|
||||
encodeWord(data, mimeWordEncoding, maxLength) {
|
||||
mimeWordEncoding = (mimeWordEncoding || 'Q').toString().toUpperCase().trim().charAt(0);
|
||||
maxLength = maxLength || 0;
|
||||
|
||||
let encodedStr;
|
||||
let toCharset = 'UTF-8';
|
||||
|
||||
if (maxLength && maxLength > 7 + toCharset.length) {
|
||||
maxLength -= 7 + toCharset.length;
|
||||
}
|
||||
|
||||
if (mimeWordEncoding === 'Q') {
|
||||
// https://tools.ietf.org/html/rfc2047#section-5 rule (3)
|
||||
encodedStr = qp.encode(data).replace(/[^a-z0-9!*+\-/=]/gi, chr => {
|
||||
let ord = chr.charCodeAt(0).toString(16).toUpperCase();
|
||||
if (chr === ' ') {
|
||||
return '_';
|
||||
} else {
|
||||
return '=' + (ord.length === 1 ? '0' + ord : ord);
|
||||
}
|
||||
});
|
||||
} else if (mimeWordEncoding === 'B') {
|
||||
encodedStr = typeof data === 'string' ? data : base64.encode(data);
|
||||
maxLength = maxLength ? Math.max(3, ((maxLength - (maxLength % 4)) / 4) * 3) : 0;
|
||||
}
|
||||
|
||||
if (maxLength && (mimeWordEncoding !== 'B' ? encodedStr : base64.encode(data)).length > maxLength) {
|
||||
if (mimeWordEncoding === 'Q') {
|
||||
encodedStr = this.splitMimeEncodedString(encodedStr, maxLength).join('?= =?' + toCharset + '?' + mimeWordEncoding + '?');
|
||||
} else {
|
||||
// RFC2047 6.3 (2) states that encoded-word must include an integral number of characters, so no chopping unicode sequences
|
||||
let parts = [];
|
||||
let lpart = '';
|
||||
for (let i = 0, len = encodedStr.length; i < len; i++) {
|
||||
let chr = encodedStr.charAt(i);
|
||||
|
||||
if (/[\ud83c\ud83d\ud83e]/.test(chr) && i < len - 1) {
|
||||
// composite emoji byte, so add the next byte as well
|
||||
chr += encodedStr.charAt(++i);
|
||||
}
|
||||
|
||||
// check if we can add this character to the existing string
|
||||
// without breaking byte length limit
|
||||
if (Buffer.byteLength(lpart + chr) <= maxLength || i === 0) {
|
||||
lpart += chr;
|
||||
} else {
|
||||
// we hit the length limit, so push the existing string and start over
|
||||
parts.push(base64.encode(lpart));
|
||||
lpart = chr;
|
||||
}
|
||||
}
|
||||
if (lpart) {
|
||||
parts.push(base64.encode(lpart));
|
||||
}
|
||||
|
||||
if (parts.length > 1) {
|
||||
encodedStr = parts.join('?= =?' + toCharset + '?' + mimeWordEncoding + '?');
|
||||
} else {
|
||||
encodedStr = parts.join('');
|
||||
}
|
||||
}
|
||||
} else if (mimeWordEncoding === 'B') {
|
||||
encodedStr = base64.encode(data);
|
||||
}
|
||||
|
||||
return '=?' + toCharset + '?' + mimeWordEncoding + '?' + encodedStr + (encodedStr.substr(-2) === '?=' ? '' : '?=');
|
||||
},
|
||||
|
||||
/**
|
||||
* Finds word sequences with non ascii text and converts these to mime words
|
||||
*
|
||||
* @param {String} value String to be encoded
|
||||
* @param {String} mimeWordEncoding='Q' Encoding for the mime word, either Q or B
|
||||
* @param {Number} [maxLength=0] If set, split mime words into several chunks if needed
|
||||
* @param {Boolean} [encodeAll=false] If true and the value needs encoding then encodes entire string, not just the smallest match
|
||||
* @return {String} String with possible mime words
|
||||
*/
|
||||
encodeWords(value, mimeWordEncoding, maxLength, encodeAll) {
|
||||
maxLength = maxLength || 0;
|
||||
|
||||
let encodedValue;
|
||||
|
||||
// find first word with a non-printable ascii or special symbol in it
|
||||
let firstMatch = value.match(/(?:^|\s)([^\s]*["\u0080-\uFFFF])/);
|
||||
if (!firstMatch) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (encodeAll) {
|
||||
// if it is requested to encode everything or the string contains something that resebles encoded word, then encode everything
|
||||
|
||||
return this.encodeWord(value, mimeWordEncoding, maxLength);
|
||||
}
|
||||
|
||||
// find the last word with a non-printable ascii in it
|
||||
let lastMatch = value.match(/(["\u0080-\uFFFF][^\s]*)[^"\u0080-\uFFFF]*$/);
|
||||
if (!lastMatch) {
|
||||
// should not happen
|
||||
return value;
|
||||
}
|
||||
|
||||
let startIndex =
|
||||
firstMatch.index +
|
||||
(
|
||||
firstMatch[0].match(/[^\s]/) || {
|
||||
index: 0
|
||||
}
|
||||
).index;
|
||||
let endIndex = lastMatch.index + (lastMatch[1] || '').length;
|
||||
|
||||
encodedValue =
|
||||
(startIndex ? value.substr(0, startIndex) : '') +
|
||||
this.encodeWord(value.substring(startIndex, endIndex), mimeWordEncoding || 'Q', maxLength) +
|
||||
(endIndex < value.length ? value.substr(endIndex) : '');
|
||||
|
||||
return encodedValue;
|
||||
},
|
||||
|
||||
/**
|
||||
* Joins parsed header value together as 'value; param1=value1; param2=value2'
|
||||
* PS: We are following RFC 822 for the list of special characters that we need to keep in quotes.
|
||||
* Refer: https://www.w3.org/Protocols/rfc1341/4_Content-Type.html
|
||||
* @param {Object} structured Parsed header value
|
||||
* @return {String} joined header value
|
||||
*/
|
||||
buildHeaderValue(structured) {
|
||||
let paramsArray = [];
|
||||
|
||||
Object.keys(structured.params || {}).forEach(param => {
|
||||
// filename might include unicode characters so it is a special case
|
||||
// other values probably do not
|
||||
let value = structured.params[param];
|
||||
if (!this.isPlainText(value, true) || value.length >= 75) {
|
||||
this.buildHeaderParam(param, value, 50).forEach(encodedParam => {
|
||||
if (!/[\s"\\;:/=(),<>@[\]?]|^[-']|'$/.test(encodedParam.value) || encodedParam.key.substr(-1) === '*') {
|
||||
paramsArray.push(encodedParam.key + '=' + encodedParam.value);
|
||||
} else {
|
||||
paramsArray.push(encodedParam.key + '=' + JSON.stringify(encodedParam.value));
|
||||
}
|
||||
});
|
||||
} else if (/[\s'"\\;:/=(),<>@[\]?]|^-/.test(value)) {
|
||||
paramsArray.push(param + '=' + JSON.stringify(value));
|
||||
} else {
|
||||
paramsArray.push(param + '=' + value);
|
||||
}
|
||||
});
|
||||
|
||||
return structured.value + (paramsArray.length ? '; ' + paramsArray.join('; ') : '');
|
||||
},
|
||||
|
||||
/**
|
||||
* Encodes a string or an Buffer to an UTF-8 Parameter Value Continuation encoding (rfc2231)
|
||||
* Useful for splitting long parameter values.
|
||||
*
|
||||
* For example
|
||||
* title="unicode string"
|
||||
* becomes
|
||||
* title*0*=utf-8''unicode
|
||||
* title*1*=%20string
|
||||
*
|
||||
* @param {String|Buffer} data String to be encoded
|
||||
* @param {Number} [maxLength=50] Max length for generated chunks
|
||||
* @param {String} [fromCharset='UTF-8'] Source sharacter set
|
||||
* @return {Array} A list of encoded keys and headers
|
||||
*/
|
||||
buildHeaderParam(key, data, maxLength) {
|
||||
let list = [];
|
||||
let encodedStr = typeof data === 'string' ? data : (data || '').toString();
|
||||
let encodedStrArr;
|
||||
let chr, ord;
|
||||
let line;
|
||||
let startPos = 0;
|
||||
let i, len;
|
||||
|
||||
maxLength = maxLength || 50;
|
||||
|
||||
// process ascii only text
|
||||
if (this.isPlainText(data, true)) {
|
||||
// check if conversion is even needed
|
||||
if (encodedStr.length <= maxLength) {
|
||||
return [
|
||||
{
|
||||
key,
|
||||
value: encodedStr
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
encodedStr = encodedStr.replace(new RegExp('.{' + maxLength + '}', 'g'), str => {
|
||||
list.push({
|
||||
line: str
|
||||
});
|
||||
return '';
|
||||
});
|
||||
|
||||
if (encodedStr) {
|
||||
list.push({
|
||||
line: encodedStr
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (/[\uD800-\uDBFF]/.test(encodedStr)) {
|
||||
// string containts surrogate pairs, so normalize it to an array of bytes
|
||||
encodedStrArr = [];
|
||||
for (i = 0, len = encodedStr.length; i < len; i++) {
|
||||
chr = encodedStr.charAt(i);
|
||||
ord = chr.charCodeAt(0);
|
||||
if (ord >= 0xd800 && ord <= 0xdbff && i < len - 1) {
|
||||
chr += encodedStr.charAt(i + 1);
|
||||
encodedStrArr.push(chr);
|
||||
i++;
|
||||
} else {
|
||||
encodedStrArr.push(chr);
|
||||
}
|
||||
}
|
||||
encodedStr = encodedStrArr;
|
||||
}
|
||||
|
||||
// first line includes the charset and language info and needs to be encoded
|
||||
// even if it does not contain any unicode characters
|
||||
line = "utf-8''";
|
||||
let encoded = true;
|
||||
startPos = 0;
|
||||
|
||||
// process text with unicode or special chars
|
||||
for (i = 0, len = encodedStr.length; i < len; i++) {
|
||||
chr = encodedStr[i];
|
||||
|
||||
if (encoded) {
|
||||
chr = this.safeEncodeURIComponent(chr);
|
||||
} else {
|
||||
// try to urlencode current char
|
||||
chr = chr === ' ' ? chr : this.safeEncodeURIComponent(chr);
|
||||
// By default it is not required to encode a line, the need
|
||||
// only appears when the string contains unicode or special chars
|
||||
// in this case we start processing the line over and encode all chars
|
||||
if (chr !== encodedStr[i]) {
|
||||
// Check if it is even possible to add the encoded char to the line
|
||||
// If not, there is no reason to use this line, just push it to the list
|
||||
// and start a new line with the char that needs encoding
|
||||
if ((this.safeEncodeURIComponent(line) + chr).length >= maxLength) {
|
||||
list.push({
|
||||
line,
|
||||
encoded
|
||||
});
|
||||
line = '';
|
||||
startPos = i - 1;
|
||||
} else {
|
||||
encoded = true;
|
||||
i = startPos;
|
||||
line = '';
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if the line is already too long, push it to the list and start a new one
|
||||
if ((line + chr).length >= maxLength) {
|
||||
list.push({
|
||||
line,
|
||||
encoded
|
||||
});
|
||||
line = chr = encodedStr[i] === ' ' ? ' ' : this.safeEncodeURIComponent(encodedStr[i]);
|
||||
if (chr === encodedStr[i]) {
|
||||
encoded = false;
|
||||
startPos = i - 1;
|
||||
} else {
|
||||
encoded = true;
|
||||
}
|
||||
} else {
|
||||
line += chr;
|
||||
}
|
||||
}
|
||||
|
||||
if (line) {
|
||||
list.push({
|
||||
line,
|
||||
encoded
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return list.map((item, i) => ({
|
||||
// encoded lines: {name}*{part}*
|
||||
// unencoded lines: {name}*{part}
|
||||
// if any line needs to be encoded then the first line (part==0) is always encoded
|
||||
key: key + '*' + i + (item.encoded ? '*' : ''),
|
||||
value: item.line
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
* Parses a header value with key=value arguments into a structured
|
||||
* object.
|
||||
*
|
||||
* parseHeaderValue('content-type: text/plain; CHARSET='UTF-8'') ->
|
||||
* {
|
||||
* 'value': 'text/plain',
|
||||
* 'params': {
|
||||
* 'charset': 'UTF-8'
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @param {String} str Header value
|
||||
* @return {Object} Header value as a parsed structure
|
||||
*/
|
||||
parseHeaderValue(str) {
|
||||
let response = {
|
||||
value: false,
|
||||
params: {}
|
||||
};
|
||||
let key = false;
|
||||
let value = '';
|
||||
let type = 'value';
|
||||
let quote = false;
|
||||
let escaped = false;
|
||||
let chr;
|
||||
|
||||
for (let i = 0, len = str.length; i < len; i++) {
|
||||
chr = str.charAt(i);
|
||||
if (type === 'key') {
|
||||
if (chr === '=') {
|
||||
key = value.trim().toLowerCase();
|
||||
type = 'value';
|
||||
value = '';
|
||||
continue;
|
||||
}
|
||||
value += chr;
|
||||
} else {
|
||||
if (escaped) {
|
||||
value += chr;
|
||||
} else if (chr === '\\') {
|
||||
escaped = true;
|
||||
continue;
|
||||
} else if (quote && chr === quote) {
|
||||
quote = false;
|
||||
} else if (!quote && chr === '"') {
|
||||
quote = chr;
|
||||
} else if (!quote && chr === ';') {
|
||||
if (key === false) {
|
||||
response.value = value.trim();
|
||||
} else {
|
||||
response.params[key] = value.trim();
|
||||
}
|
||||
type = 'key';
|
||||
value = '';
|
||||
} else {
|
||||
value += chr;
|
||||
}
|
||||
escaped = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'value') {
|
||||
if (key === false) {
|
||||
response.value = value.trim();
|
||||
} else {
|
||||
response.params[key] = value.trim();
|
||||
}
|
||||
} else if (value.trim()) {
|
||||
response.params[value.trim().toLowerCase()] = '';
|
||||
}
|
||||
|
||||
// handle parameter value continuations
|
||||
// https://tools.ietf.org/html/rfc2231#section-3
|
||||
|
||||
// preprocess values
|
||||
Object.keys(response.params).forEach(key => {
|
||||
let actualKey, nr, match, value;
|
||||
if ((match = key.match(/(\*(\d+)|\*(\d+)\*|\*)$/))) {
|
||||
actualKey = key.substr(0, match.index);
|
||||
nr = Number(match[2] || match[3]) || 0;
|
||||
|
||||
if (!response.params[actualKey] || typeof response.params[actualKey] !== 'object') {
|
||||
response.params[actualKey] = {
|
||||
charset: false,
|
||||
values: []
|
||||
};
|
||||
}
|
||||
|
||||
value = response.params[key];
|
||||
|
||||
if (nr === 0 && match[0].substr(-1) === '*' && (match = value.match(/^([^']*)'[^']*'(.*)$/))) {
|
||||
response.params[actualKey].charset = match[1] || 'iso-8859-1';
|
||||
value = match[2];
|
||||
}
|
||||
|
||||
response.params[actualKey].values[nr] = value;
|
||||
|
||||
// remove the old reference
|
||||
delete response.params[key];
|
||||
}
|
||||
});
|
||||
|
||||
// concatenate split rfc2231 strings and convert encoded strings to mime encoded words
|
||||
Object.keys(response.params).forEach(key => {
|
||||
let value;
|
||||
if (response.params[key] && Array.isArray(response.params[key].values)) {
|
||||
value = response.params[key].values.map(val => val || '').join('');
|
||||
|
||||
if (response.params[key].charset) {
|
||||
// convert "%AB" to "=?charset?Q?=AB?="
|
||||
response.params[key] =
|
||||
'=?' +
|
||||
response.params[key].charset +
|
||||
'?Q?' +
|
||||
value
|
||||
// fix invalidly encoded chars
|
||||
.replace(/[=?_\s]/g, s => {
|
||||
let c = s.charCodeAt(0).toString(16);
|
||||
if (s === ' ') {
|
||||
return '_';
|
||||
} else {
|
||||
return '%' + (c.length < 2 ? '0' : '') + c;
|
||||
}
|
||||
})
|
||||
// change from urlencoding to percent encoding
|
||||
.replace(/%/g, '=') +
|
||||
'?=';
|
||||
} else {
|
||||
response.params[key] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns file extension for a content type string. If no suitable extensions
|
||||
* are found, 'bin' is used as the default extension
|
||||
*
|
||||
* @param {String} mimeType Content type to be checked for
|
||||
* @return {String} File extension
|
||||
*/
|
||||
detectExtension: mimeType => mimeTypes.detectExtension(mimeType),
|
||||
|
||||
/**
|
||||
* Returns content type for a file extension. If no suitable content types
|
||||
* are found, 'application/octet-stream' is used as the default content type
|
||||
*
|
||||
* @param {String} extension Extension to be checked for
|
||||
* @return {String} File extension
|
||||
*/
|
||||
detectMimeType: extension => mimeTypes.detectMimeType(extension),
|
||||
|
||||
/**
|
||||
* Folds long lines, useful for folding header lines (afterSpace=false) and
|
||||
* flowed text (afterSpace=true)
|
||||
*
|
||||
* @param {String} str String to be folded
|
||||
* @param {Number} [lineLength=76] Maximum length of a line
|
||||
* @param {Boolean} afterSpace If true, leave a space in th end of a line
|
||||
* @return {String} String with folded lines
|
||||
*/
|
||||
foldLines(str, lineLength, afterSpace) {
|
||||
str = (str || '').toString();
|
||||
lineLength = lineLength || 76;
|
||||
|
||||
let pos = 0,
|
||||
len = str.length,
|
||||
result = '',
|
||||
line,
|
||||
match;
|
||||
|
||||
while (pos < len) {
|
||||
line = str.substr(pos, lineLength);
|
||||
if (line.length < lineLength) {
|
||||
result += line;
|
||||
break;
|
||||
}
|
||||
if ((match = line.match(/^[^\n\r]*(\r?\n|\r)/))) {
|
||||
line = match[0];
|
||||
result += line;
|
||||
pos += line.length;
|
||||
continue;
|
||||
} else if ((match = line.match(/(\s+)[^\s]*$/)) && match[0].length - (afterSpace ? (match[1] || '').length : 0) < line.length) {
|
||||
line = line.substr(0, line.length - (match[0].length - (afterSpace ? (match[1] || '').length : 0)));
|
||||
} else if ((match = str.substr(pos + line.length).match(/^[^\s]+(\s*)/))) {
|
||||
line = line + match[0].substr(0, match[0].length - (!afterSpace ? (match[1] || '').length : 0));
|
||||
}
|
||||
|
||||
result += line;
|
||||
pos += line.length;
|
||||
if (pos < len) {
|
||||
result += '\r\n';
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Splits a mime encoded string. Needed for dividing mime words into smaller chunks
|
||||
*
|
||||
* @param {String} str Mime encoded string to be split up
|
||||
* @param {Number} maxlen Maximum length of characters for one part (minimum 12)
|
||||
* @return {Array} Split string
|
||||
*/
|
||||
splitMimeEncodedString: (str, maxlen) => {
|
||||
let curLine,
|
||||
match,
|
||||
chr,
|
||||
done,
|
||||
lines = [];
|
||||
|
||||
// require at least 12 symbols to fit possible 4 octet UTF-8 sequences
|
||||
maxlen = Math.max(maxlen || 0, 12);
|
||||
|
||||
while (str.length) {
|
||||
curLine = str.substr(0, maxlen);
|
||||
|
||||
// move incomplete escaped char back to main
|
||||
if ((match = curLine.match(/[=][0-9A-F]?$/i))) {
|
||||
curLine = curLine.substr(0, match.index);
|
||||
}
|
||||
|
||||
done = false;
|
||||
while (!done) {
|
||||
done = true;
|
||||
// check if not middle of a unicode char sequence
|
||||
if ((match = str.substr(curLine.length).match(/^[=]([0-9A-F]{2})/i))) {
|
||||
chr = parseInt(match[1], 16);
|
||||
// invalid sequence, move one char back anc recheck
|
||||
if (chr < 0xc2 && chr > 0x7f) {
|
||||
curLine = curLine.substr(0, curLine.length - 3);
|
||||
done = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (curLine.length) {
|
||||
lines.push(curLine);
|
||||
}
|
||||
str = str.substr(curLine.length);
|
||||
}
|
||||
|
||||
return lines;
|
||||
},
|
||||
|
||||
encodeURICharComponent: chr => {
|
||||
let res = '';
|
||||
let ord = chr.charCodeAt(0).toString(16).toUpperCase();
|
||||
|
||||
if (ord.length % 2) {
|
||||
ord = '0' + ord;
|
||||
}
|
||||
|
||||
if (ord.length > 2) {
|
||||
for (let i = 0, len = ord.length / 2; i < len; i++) {
|
||||
res += '%' + ord.substr(i, 2);
|
||||
}
|
||||
} else {
|
||||
res += '%' + ord;
|
||||
}
|
||||
|
||||
return res;
|
||||
},
|
||||
|
||||
safeEncodeURIComponent(str) {
|
||||
str = (str || '').toString();
|
||||
|
||||
try {
|
||||
// might throw if we try to encode invalid sequences, eg. partial emoji
|
||||
str = encodeURIComponent(str);
|
||||
} catch (_E) {
|
||||
// should never run
|
||||
return str.replace(/[^\x00-\x1F *'()<>@,;:\\"[\]?=\u007F-\uFFFF]+/g, '');
|
||||
}
|
||||
|
||||
// ensure chars that are not handled by encodeURICompent are converted as well
|
||||
return str.replace(/[\x00-\x1F *'()<>@,;:\\"[\]?=\u007F-\uFFFF]/g, chr => this.encodeURICharComponent(chr));
|
||||
}
|
||||
};
|
||||
2113
backend/node_modules/imapflow/node_modules/nodemailer/lib/mime-funcs/mime-types.js
generated
vendored
Normal file
2113
backend/node_modules/imapflow/node_modules/nodemailer/lib/mime-funcs/mime-types.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1316
backend/node_modules/imapflow/node_modules/nodemailer/lib/mime-node/index.js
generated
vendored
Normal file
1316
backend/node_modules/imapflow/node_modules/nodemailer/lib/mime-node/index.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
33
backend/node_modules/imapflow/node_modules/nodemailer/lib/mime-node/last-newline.js
generated
vendored
Normal file
33
backend/node_modules/imapflow/node_modules/nodemailer/lib/mime-node/last-newline.js
generated
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
'use strict';
|
||||
|
||||
const Transform = require('stream').Transform;
|
||||
|
||||
class LastNewline extends Transform {
|
||||
constructor() {
|
||||
super();
|
||||
this.lastByte = false;
|
||||
}
|
||||
|
||||
_transform(chunk, encoding, done) {
|
||||
if (chunk.length) {
|
||||
this.lastByte = chunk[chunk.length - 1];
|
||||
}
|
||||
|
||||
this.push(chunk);
|
||||
done();
|
||||
}
|
||||
|
||||
_flush(done) {
|
||||
if (this.lastByte === 0x0a) {
|
||||
return done();
|
||||
}
|
||||
if (this.lastByte === 0x0d) {
|
||||
this.push(Buffer.from('\n'));
|
||||
return done();
|
||||
}
|
||||
this.push(Buffer.from('\r\n'));
|
||||
return done();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LastNewline;
|
||||
43
backend/node_modules/imapflow/node_modules/nodemailer/lib/mime-node/le-unix.js
generated
vendored
Normal file
43
backend/node_modules/imapflow/node_modules/nodemailer/lib/mime-node/le-unix.js
generated
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
'use strict';
|
||||
|
||||
const stream = require('stream');
|
||||
const Transform = stream.Transform;
|
||||
|
||||
/**
|
||||
* Ensures that only <LF> is used for linebreaks
|
||||
*
|
||||
* @param {Object} options Stream options
|
||||
*/
|
||||
class LeWindows extends Transform {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
// init Transform
|
||||
this.options = options || {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes dots
|
||||
*/
|
||||
_transform(chunk, encoding, done) {
|
||||
let buf;
|
||||
let lastPos = 0;
|
||||
|
||||
for (let i = 0, len = chunk.length; i < len; i++) {
|
||||
if (chunk[i] === 0x0d) {
|
||||
// \n
|
||||
buf = chunk.slice(lastPos, i);
|
||||
lastPos = i + 1;
|
||||
this.push(buf);
|
||||
}
|
||||
}
|
||||
if (lastPos && lastPos < chunk.length) {
|
||||
buf = chunk.slice(lastPos);
|
||||
this.push(buf);
|
||||
} else if (!lastPos) {
|
||||
this.push(chunk);
|
||||
}
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LeWindows;
|
||||
52
backend/node_modules/imapflow/node_modules/nodemailer/lib/mime-node/le-windows.js
generated
vendored
Normal file
52
backend/node_modules/imapflow/node_modules/nodemailer/lib/mime-node/le-windows.js
generated
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
'use strict';
|
||||
|
||||
const stream = require('stream');
|
||||
const Transform = stream.Transform;
|
||||
|
||||
/**
|
||||
* Ensures that only <CR><LF> sequences are used for linebreaks
|
||||
*
|
||||
* @param {Object} options Stream options
|
||||
*/
|
||||
class LeWindows extends Transform {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
// init Transform
|
||||
this.options = options || {};
|
||||
this.lastByte = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes dots
|
||||
*/
|
||||
_transform(chunk, encoding, done) {
|
||||
let buf;
|
||||
let lastPos = 0;
|
||||
|
||||
for (let i = 0, len = chunk.length; i < len; i++) {
|
||||
if (chunk[i] === 0x0a) {
|
||||
// \n
|
||||
if ((i && chunk[i - 1] !== 0x0d) || (!i && this.lastByte !== 0x0d)) {
|
||||
if (i > lastPos) {
|
||||
buf = chunk.slice(lastPos, i);
|
||||
this.push(buf);
|
||||
}
|
||||
this.push(Buffer.from('\r\n'));
|
||||
lastPos = i + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lastPos && lastPos < chunk.length) {
|
||||
buf = chunk.slice(lastPos);
|
||||
this.push(buf);
|
||||
} else if (!lastPos) {
|
||||
this.push(chunk);
|
||||
}
|
||||
|
||||
this.lastByte = chunk[chunk.length - 1];
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LeWindows;
|
||||
157
backend/node_modules/imapflow/node_modules/nodemailer/lib/nodemailer.js
generated
vendored
Normal file
157
backend/node_modules/imapflow/node_modules/nodemailer/lib/nodemailer.js
generated
vendored
Normal file
@@ -0,0 +1,157 @@
|
||||
'use strict';
|
||||
|
||||
const Mailer = require('./mailer');
|
||||
const shared = require('./shared');
|
||||
const SMTPPool = require('./smtp-pool');
|
||||
const SMTPTransport = require('./smtp-transport');
|
||||
const SendmailTransport = require('./sendmail-transport');
|
||||
const StreamTransport = require('./stream-transport');
|
||||
const JSONTransport = require('./json-transport');
|
||||
const SESTransport = require('./ses-transport');
|
||||
const nmfetch = require('./fetch');
|
||||
const packageData = require('../package.json');
|
||||
|
||||
const ETHEREAL_API = (process.env.ETHEREAL_API || 'https://api.nodemailer.com').replace(/\/+$/, '');
|
||||
const ETHEREAL_WEB = (process.env.ETHEREAL_WEB || 'https://ethereal.email').replace(/\/+$/, '');
|
||||
const ETHEREAL_API_KEY = (process.env.ETHEREAL_API_KEY || '').replace(/\s*/g, '') || null;
|
||||
const ETHEREAL_CACHE = ['true', 'yes', 'y', '1'].includes((process.env.ETHEREAL_CACHE || 'yes').toString().trim().toLowerCase());
|
||||
|
||||
let testAccount = false;
|
||||
|
||||
module.exports.createTransport = function (transporter, defaults) {
|
||||
let urlConfig;
|
||||
let options;
|
||||
let mailer;
|
||||
|
||||
if (
|
||||
// provided transporter is a configuration object, not transporter plugin
|
||||
(typeof transporter === 'object' && typeof transporter.send !== 'function') ||
|
||||
// provided transporter looks like a connection url
|
||||
(typeof transporter === 'string' && /^(smtps?|direct):/i.test(transporter))
|
||||
) {
|
||||
if ((urlConfig = typeof transporter === 'string' ? transporter : transporter.url)) {
|
||||
// parse a configuration URL into configuration options
|
||||
options = shared.parseConnectionUrl(urlConfig);
|
||||
} else {
|
||||
options = transporter;
|
||||
}
|
||||
|
||||
if (options.pool) {
|
||||
transporter = new SMTPPool(options);
|
||||
} else if (options.sendmail) {
|
||||
transporter = new SendmailTransport(options);
|
||||
} else if (options.streamTransport) {
|
||||
transporter = new StreamTransport(options);
|
||||
} else if (options.jsonTransport) {
|
||||
transporter = new JSONTransport(options);
|
||||
} else if (options.SES) {
|
||||
if (options.SES.ses && options.SES.aws) {
|
||||
let error = new Error(
|
||||
'Using legacy SES configuration, expecting @aws-sdk/client-sesv2, see https://nodemailer.com/transports/ses/'
|
||||
);
|
||||
error.code = 'LegacyConfig';
|
||||
throw error;
|
||||
}
|
||||
transporter = new SESTransport(options);
|
||||
} else {
|
||||
transporter = new SMTPTransport(options);
|
||||
}
|
||||
}
|
||||
|
||||
mailer = new Mailer(transporter, options, defaults);
|
||||
|
||||
return mailer;
|
||||
};
|
||||
|
||||
module.exports.createTestAccount = function (apiUrl, callback) {
|
||||
let promise;
|
||||
|
||||
if (!callback && typeof apiUrl === 'function') {
|
||||
callback = apiUrl;
|
||||
apiUrl = false;
|
||||
}
|
||||
|
||||
if (!callback) {
|
||||
promise = new Promise((resolve, reject) => {
|
||||
callback = shared.callbackPromise(resolve, reject);
|
||||
});
|
||||
}
|
||||
|
||||
if (ETHEREAL_CACHE && testAccount) {
|
||||
setImmediate(() => callback(null, testAccount));
|
||||
return promise;
|
||||
}
|
||||
|
||||
apiUrl = apiUrl || ETHEREAL_API;
|
||||
|
||||
let chunks = [];
|
||||
let chunklen = 0;
|
||||
|
||||
let requestHeaders = {};
|
||||
let requestBody = {
|
||||
requestor: packageData.name,
|
||||
version: packageData.version
|
||||
};
|
||||
|
||||
if (ETHEREAL_API_KEY) {
|
||||
requestHeaders.Authorization = 'Bearer ' + ETHEREAL_API_KEY;
|
||||
}
|
||||
|
||||
let req = nmfetch(apiUrl + '/user', {
|
||||
contentType: 'application/json',
|
||||
method: 'POST',
|
||||
headers: requestHeaders,
|
||||
body: Buffer.from(JSON.stringify(requestBody))
|
||||
});
|
||||
|
||||
req.on('readable', () => {
|
||||
let chunk;
|
||||
while ((chunk = req.read()) !== null) {
|
||||
chunks.push(chunk);
|
||||
chunklen += chunk.length;
|
||||
}
|
||||
});
|
||||
|
||||
req.once('error', err => callback(err));
|
||||
|
||||
req.once('end', () => {
|
||||
let res = Buffer.concat(chunks, chunklen);
|
||||
let data;
|
||||
let err;
|
||||
try {
|
||||
data = JSON.parse(res.toString());
|
||||
} catch (E) {
|
||||
err = E;
|
||||
}
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (data.status !== 'success' || data.error) {
|
||||
return callback(new Error(data.error || 'Request failed'));
|
||||
}
|
||||
delete data.status;
|
||||
testAccount = data;
|
||||
callback(null, testAccount);
|
||||
});
|
||||
|
||||
return promise;
|
||||
};
|
||||
|
||||
module.exports.getTestMessageUrl = function (info) {
|
||||
if (!info || !info.response) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let infoProps = new Map();
|
||||
info.response.replace(/\[([^\]]+)\]$/, (m, props) => {
|
||||
props.replace(/\b([A-Z0-9]+)=([^\s]+)/g, (m, key, value) => {
|
||||
infoProps.set(key, value);
|
||||
});
|
||||
});
|
||||
|
||||
if (infoProps.has('STATUS') && infoProps.has('MSGID')) {
|
||||
return (testAccount.web || ETHEREAL_WEB) + '/message/' + infoProps.get('MSGID');
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user