├── .eslintignore ├── tutorials ├── emails │ ├── emails.md │ └── move-inbox-to-spam-for-viagra.md ├── labels │ ├── labels.md │ ├── create-label.md │ ├── delete-label.md │ └── tag-untag.md ├── folders │ ├── folders.md │ ├── create-folder.md │ ├── delete-folder.md │ └── move.md ├── send-email.md ├── conversations │ └── conversations.md └── meta.json ├── commitlint.config.js ├── index.js ├── docs ├── fonts │ ├── Montserrat │ │ ├── Montserrat-Bold.eot │ │ ├── Montserrat-Bold.ttf │ │ ├── Montserrat-Bold.woff │ │ ├── Montserrat-Bold.woff2 │ │ ├── Montserrat-Regular.eot │ │ ├── Montserrat-Regular.ttf │ │ ├── Montserrat-Regular.woff │ │ └── Montserrat-Regular.woff2 │ └── Source-Sans-Pro │ │ ├── sourcesanspro-light-webfont.eot │ │ ├── sourcesanspro-light-webfont.ttf │ │ ├── sourcesanspro-light-webfont.woff │ │ ├── sourcesanspro-light-webfont.woff2 │ │ ├── sourcesanspro-regular-webfont.eot │ │ ├── sourcesanspro-regular-webfont.ttf │ │ ├── sourcesanspro-regular-webfont.woff │ │ └── sourcesanspro-regular-webfont.woff2 ├── scripts │ ├── polyfill.js │ ├── nav.js │ ├── linenumber.js │ ├── collapse.js │ ├── prettify │ │ ├── lang-css.js │ │ ├── Apache-License-2.0.txt │ │ └── prettify.js │ └── search.js ├── styles │ ├── prettify.css │ └── jsdoc.css ├── tutorial-emails.html ├── tutorial-labels.html ├── tutorial-send-email.html ├── tutorial-conversations.html ├── tutorial-folders.html ├── tutorial-create-label.html ├── tutorial-create-folder.html ├── tutorial-delete-label.html ├── tutorial-delete-folder.html ├── tutorial-move-inbox-to-spam-for-viagra.html ├── tutorial-move.html ├── tutorial-tag-untag.html ├── index.html ├── Address.html ├── Label.html └── Folder.html ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── lib ├── address.js ├── label.js ├── folder.js ├── conversation.js ├── email.js └── proton-mail.js ├── tests └── integration │ ├── support.js │ ├── proton-mail.test.js │ ├── folder.test.js │ ├── label.test.js │ ├── conversation.test.js │ └── email.test.js ├── DOCS_README.md ├── .jsdoc.json ├── LICENSE ├── CHANGELOG.md ├── package.json ├── README.md └── types.d.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | docs/** -------------------------------------------------------------------------------- /tutorials/emails/emails.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tutorials/labels/labels.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] } 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const ProtonMail = require('./lib/proton-mail') 2 | 3 | module.exports = ProtonMail 4 | -------------------------------------------------------------------------------- /docs/fonts/Montserrat/Montserrat-Bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jusguy/protonmail-api/HEAD/docs/fonts/Montserrat/Montserrat-Bold.eot -------------------------------------------------------------------------------- /docs/fonts/Montserrat/Montserrat-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jusguy/protonmail-api/HEAD/docs/fonts/Montserrat/Montserrat-Bold.ttf -------------------------------------------------------------------------------- /docs/fonts/Montserrat/Montserrat-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jusguy/protonmail-api/HEAD/docs/fonts/Montserrat/Montserrat-Bold.woff -------------------------------------------------------------------------------- /docs/fonts/Montserrat/Montserrat-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jusguy/protonmail-api/HEAD/docs/fonts/Montserrat/Montserrat-Bold.woff2 -------------------------------------------------------------------------------- /docs/fonts/Montserrat/Montserrat-Regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jusguy/protonmail-api/HEAD/docs/fonts/Montserrat/Montserrat-Regular.eot -------------------------------------------------------------------------------- /docs/fonts/Montserrat/Montserrat-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jusguy/protonmail-api/HEAD/docs/fonts/Montserrat/Montserrat-Regular.ttf -------------------------------------------------------------------------------- /docs/fonts/Montserrat/Montserrat-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jusguy/protonmail-api/HEAD/docs/fonts/Montserrat/Montserrat-Regular.woff -------------------------------------------------------------------------------- /docs/fonts/Montserrat/Montserrat-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jusguy/protonmail-api/HEAD/docs/fonts/Montserrat/Montserrat-Regular.woff2 -------------------------------------------------------------------------------- /docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jusguy/protonmail-api/HEAD/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jusguy/protonmail-api/HEAD/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf -------------------------------------------------------------------------------- /docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jusguy/protonmail-api/HEAD/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jusguy/protonmail-api/HEAD/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 -------------------------------------------------------------------------------- /docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jusguy/protonmail-api/HEAD/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jusguy/protonmail-api/HEAD/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf -------------------------------------------------------------------------------- /docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jusguy/protonmail-api/HEAD/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jusguy/protonmail-api/HEAD/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 -------------------------------------------------------------------------------- /tutorials/folders/folders.md: -------------------------------------------------------------------------------- 1 | An email or conversation can only be in one folder at a time (unlike labels). 2 | 3 | ProtonMail provides default folders such as `inbox`, `trash`, or `spam`. Custom folders can also be created. -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | commit-message: 8 | prefix: "build" 9 | include: "scope" -------------------------------------------------------------------------------- /docs/scripts/polyfill.js: -------------------------------------------------------------------------------- 1 | //IE Fix, src: https://www.reddit.com/r/programminghorror/comments/6abmcr/nodelist_lacks_foreach_in_internet_explorer/ 2 | if (typeof(NodeList.prototype.forEach)!==typeof(alert)){ 3 | NodeList.prototype.forEach=Array.prototype.forEach; 4 | } -------------------------------------------------------------------------------- /tutorials/send-email.md: -------------------------------------------------------------------------------- 1 | ```js 2 | const ProtonMail = require('protonmail-api'); 3 | 4 | (async () => { 5 | const pm = await ProtonMail.connect({ 6 | username: 'foobar@protonmail.com', 7 | password: 'somethingsecure' 8 | }) 9 | 10 | await pm.sendEmail({ 11 | to: 'justin@kalland.ch', 12 | subject: 'Send email tutorial', 13 | body: 'Hello world' 14 | }) 15 | 16 | pm.close() 17 | })() 18 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # MacOS 2 | .DS_Store* 3 | Icon? 4 | ._* 5 | 6 | # Windows 7 | Thumbs.db 8 | ehthumbs.db 9 | Desktop.ini 10 | 11 | # Linux 12 | .directory 13 | *~ 14 | 15 | # Temp paths 16 | tmp/ 17 | temp/ 18 | 19 | # Editors 20 | .idea/ 21 | .vscode/ 22 | 23 | # TypeScript 24 | dist/ 25 | 26 | # npm 27 | node_modules/ 28 | *.log 29 | *.gz 30 | 31 | # jest & nyc 32 | coverage/ 33 | .nyc_output/ 34 | 35 | # dotenv 36 | .env 37 | -------------------------------------------------------------------------------- /docs/scripts/nav.js: -------------------------------------------------------------------------------- 1 | function scrollToNavItem() { 2 | var path = window.location.href.split('/').pop().replace(/\.html/, ''); 3 | document.querySelectorAll('nav a').forEach(function(link) { 4 | var href = link.attributes.href.value.replace(/\.html/, ''); 5 | if (path === href) { 6 | link.scrollIntoView({block: 'center'}); 7 | return; 8 | } 9 | }) 10 | } 11 | 12 | scrollToNavItem(); 13 | -------------------------------------------------------------------------------- /tutorials/conversations/conversations.md: -------------------------------------------------------------------------------- 1 | A conversation is a group of related emails. Algorithms on the server side of ProtonMail determine how to group emails into conversations. Conversations have many similar properties as individual emails, such as labels, moving folders, and stars. With this API you can choose to work with individual emails, or with conversations. Both can be fetched and sorted in a similar fashion. Working with individual emails is recommended because it is more predictable (conversation grouping is a black box). -------------------------------------------------------------------------------- /tutorials/labels/create-label.md: -------------------------------------------------------------------------------- 1 | ```js 2 | const ProtonMail = require('protonmail-api'); 3 | 4 | (async () => { 5 | const pm = await ProtonMail.connect({ 6 | username: 'foobar@protonmail.com', 7 | password: 'somethingsecure' 8 | }) 9 | 10 | // Create a new label 11 | const label = await pm.createLabel('foo bar') 12 | console.log(`Label ${label.name} was created and has ID ${label.id}`) 13 | 14 | pm.close() 15 | })() 16 | ``` 17 | 18 | ### Further resources: 19 | - {@link ProtonMail#createLabel|createLabel method} 20 | - {@link Label|Label class} -------------------------------------------------------------------------------- /tutorials/folders/create-folder.md: -------------------------------------------------------------------------------- 1 | ```js 2 | const ProtonMail = require('protonmail-api'); 3 | 4 | (async () => { 5 | const pm = await ProtonMail.connect({ 6 | username: 'foobar@protonmail.com', 7 | password: 'somethingsecure' 8 | }) 9 | 10 | // Create a new folder 11 | const folder = await pm.createFolder('foo bar folder') 12 | console.log(`Folder ${folder.name} was created and has ID ${folder.id}`) 13 | 14 | pm.close() 15 | })() 16 | ``` 17 | 18 | ### Further resources: 19 | - {@link ProtonMail#createLabel|createFolder method} 20 | - {@link Folder|Folder class} -------------------------------------------------------------------------------- /tutorials/labels/delete-label.md: -------------------------------------------------------------------------------- 1 | ```js 2 | const ProtonMail = require('protonmail-api'); 3 | 4 | (async () => { 5 | const pm = await ProtonMail.connect({ 6 | username: 'foobar@protonmail.com', 7 | password: 'somethingsecure' 8 | }) 9 | 10 | // Get label object by name 11 | const label = pm.getLabelByName('foo bar') 12 | 13 | // Delete the label 14 | await label.delete() 15 | console.log(`Label ${label.name} was deleted`) 16 | 17 | pm.close() 18 | })() 19 | ``` 20 | 21 | ### Further resources: 22 | - {@link ProtonMail#getLabelByName|getLabelByName method} 23 | - {@link Label|Label class} -------------------------------------------------------------------------------- /lib/address.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @hideconstructor 3 | */ 4 | class Address { 5 | constructor (protonMail, data) { 6 | this._protonMail = protonMail // keeping this for future support of things like address book 7 | /** 8 | * The email address (example: justin@kalland.ch) 9 | * @type {string} 10 | */ 11 | this.email = String(data.Address) 12 | 13 | /** 14 | * The display name (example: Justin Kalland) 15 | * @type {string} 16 | */ 17 | this.name = data.Name 18 | } 19 | 20 | toString () { 21 | return this.email 22 | } 23 | } 24 | 25 | module.exports = Address 26 | -------------------------------------------------------------------------------- /tutorials/folders/delete-folder.md: -------------------------------------------------------------------------------- 1 | ```js 2 | const ProtonMail = require('protonmail-api'); 3 | 4 | (async () => { 5 | const pm = await ProtonMail.connect({ 6 | username: 'foobar@protonmail.com', 7 | password: 'somethingsecure' 8 | }) 9 | 10 | // Get folder object by name 11 | const folder = pm.getFolderByName('foo bar folder') 12 | 13 | // Delete the label 14 | await folder.delete() 15 | console.log(`Folder ${folder.name} was deleted`) 16 | 17 | pm.close() 18 | })() 19 | ``` 20 | 21 | ### Further resources: 22 | - {@link ProtonMail#getFolderByName|getFolderByName method} 23 | - {@link Folder|Folder class} -------------------------------------------------------------------------------- /tests/integration/support.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto') 2 | const ProtonMail = require('../../lib/proton-mail') 3 | require('dotenv').config() 4 | const username = process.env.PM_USERNAME 5 | const password = process.env.PM_PASSWORD 6 | const pm = new ProtonMail({ 7 | username, 8 | password 9 | }) 10 | 11 | before(async () => { 12 | await pm._connect() 13 | }) 14 | 15 | after(() => { 16 | pm.close() 17 | }) 18 | 19 | function randomString (length = 20) { 20 | return crypto.randomBytes(length).toString('hex') 21 | } 22 | 23 | module.exports = { 24 | pm, 25 | randomString, 26 | username, 27 | password 28 | } 29 | -------------------------------------------------------------------------------- /tutorials/emails/move-inbox-to-spam-for-viagra.md: -------------------------------------------------------------------------------- 1 | ```js 2 | const ProtonMail = require('protonmail-api'); 3 | 4 | (async () => { 5 | const pm = await ProtonMail.connect({ 6 | username: 'foobar@protonmail.com', 7 | password: 'somethingsecure' 8 | }) 9 | 10 | // Get emails in inbox 11 | const emailsInInbox = await pm.getEmails('inbox') 12 | 13 | for (const email of emailsInInbox) { 14 | const body = await email.getBody() 15 | 16 | // Move the email to spam if the body contains 'viagra' 17 | if (body.includes('viagra')) { 18 | email.move('spam') 19 | } 20 | } 21 | 22 | pm.close() 23 | })() 24 | ``` 25 | 26 | ### Further resources: 27 | - {@link ProtonMail#getEmails|getEmails method} 28 | - {@link Email|Email class} -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | lint: 11 | name: Lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v1 16 | with: 17 | node-version: '16' 18 | - run: npm ci 19 | - run: npm run lint 20 | test: 21 | name: Test 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v2 25 | - uses: actions/setup-node@v1 26 | with: 27 | node-version: '16' 28 | - run: npm ci 29 | - run: npm run test 30 | env: 31 | PM_USERNAME: ${{ secrets.PM_USERNAME }} 32 | PM_PASSWORD: ${{ secrets.PM_PASSWORD }} -------------------------------------------------------------------------------- /docs/scripts/linenumber.js: -------------------------------------------------------------------------------- 1 | /*global document */ 2 | (function() { 3 | var source = document.getElementsByClassName('prettyprint source linenums'); 4 | var i = 0; 5 | var lineNumber = 0; 6 | var lineId; 7 | var lines; 8 | var totalLines; 9 | var anchorHash; 10 | 11 | if (source && source[0]) { 12 | anchorHash = document.location.hash.substring(1); 13 | lines = source[0].getElementsByTagName('li'); 14 | totalLines = lines.length; 15 | 16 | for (; i < totalLines; i++) { 17 | lineNumber++; 18 | lineId = 'line' + lineNumber; 19 | lines[i].id = lineId; 20 | if (lineId === anchorHash) { 21 | lines[i].className += ' selected'; 22 | } 23 | } 24 | } 25 | })(); 26 | -------------------------------------------------------------------------------- /lib/label.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @hideconstructor 3 | */ 4 | class Label { 5 | constructor (protonMail, data) { 6 | this._protonMail = protonMail 7 | /** 8 | * The unique identifier of the label. 9 | * @type {string} 10 | */ 11 | this.id = data.ID 12 | 13 | /** 14 | * The name of the label. 15 | * @type {string} 16 | */ 17 | this.name = data.Name 18 | } 19 | 20 | /** 21 | * Permanently delete - this does not delete associated messages/conversations. 22 | */ 23 | async delete () { 24 | await this._protonMail._page.evaluate(id => { 25 | return window.labelModel.remove(id) 26 | }, this.id) 27 | 28 | this._protonMail.labels = this._protonMail.labels.filter(label => { 29 | return label.id !== this.id 30 | }) 31 | } 32 | } 33 | 34 | module.exports = Label 35 | -------------------------------------------------------------------------------- /docs/scripts/collapse.js: -------------------------------------------------------------------------------- 1 | function hideAllButCurrent(){ 2 | //by default all submenut items are hidden 3 | //but we need to rehide them for search 4 | document.querySelectorAll("nav > ul > li > ul li").forEach(function(parent) { 5 | parent.style.display = "none"; 6 | }); 7 | 8 | //only current page (if it exists) should be opened 9 | var file = window.location.pathname.split("/").pop().replace(/\.html/, ''); 10 | document.querySelectorAll("nav > ul > li > a").forEach(function(parent) { 11 | var href = parent.attributes.href.value.replace(/\.html/, ''); 12 | if (file === href) { 13 | parent.parentNode.querySelectorAll("ul li").forEach(function(elem) { 14 | elem.style.display = "block"; 15 | }); 16 | } 17 | }); 18 | } 19 | 20 | hideAllButCurrent(); -------------------------------------------------------------------------------- /docs/scripts/prettify/lang-css.js: -------------------------------------------------------------------------------- 1 | PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n "]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com", 2 | /^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]); 3 | -------------------------------------------------------------------------------- /tutorials/folders/move.md: -------------------------------------------------------------------------------- 1 | ```js 2 | const ProtonMail = require('protonmail-api'); 3 | 4 | (async () => { 5 | const pm = await ProtonMail.connect({ 6 | username: 'foobar@protonmail.com', 7 | password: 'somethingsecure' 8 | }) 9 | 10 | // Get the first email in the inbox 11 | const emailsInInbox = await pm.getEmails('inbox') 12 | const email = emailsInInbox[0] 13 | 14 | // Move the email to the trash 15 | await email.move('trash') 16 | console.log(`The email now is now in: ${email.folder.name}`) 17 | 18 | pm.close() 19 | })() 20 | ``` 21 | 22 | Moving conversations works the same way, with the same method .move: 23 | 24 | ```js 25 | const conversationsInInbox = await pm.getConversations('inbox') 26 | const conversation = conversationsInInbox[0] 27 | await conversation.move('trash') 28 | ``` 29 | 30 | ### Further resources: 31 | - {@link Folder|Folder class} 32 | - {@link Email|Email class} 33 | - {@link Conversation|Conversation class} -------------------------------------------------------------------------------- /tests/integration/proton-mail.test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect 2 | const support = require('./support') 3 | 4 | describe('ProtonMail', () => { 5 | it('gets conversation counts', async () => { 6 | const counts = await support.pm.getConversationCounts() 7 | expect(counts).to.be.a('object') 8 | expect(counts.labels).to.be.a('object') 9 | expect(counts.folders).to.be.a('object') 10 | expect(counts.folders).to.include.all.keys('inbox', 'spam', 'trash') 11 | expect(counts.folders.inbox).to.have.all.keys('total', 'unread') 12 | }) 13 | 14 | it('gets email counts', async () => { 15 | const counts = await support.pm.getEmailCounts() 16 | 17 | expect(counts).to.be.a('object') 18 | expect(counts.labels).to.be.a('object') 19 | expect(counts.folders).to.be.a('object') 20 | expect(counts.folders).to.include.all.keys('inbox', 'spam', 'trash') 21 | expect(counts.folders.inbox).to.have.all.keys('total', 'unread') 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /DOCS_README.md: -------------------------------------------------------------------------------- 1 | Unofficial API for interacting with ProtonMail. 2 | 3 | Allows interaction with ProtonMail through a simple Node.js API. Leverages the official [WebClient](https://github.com/ProtonMail/WebClient), keeping with the spirit of security and privacy. 4 | 5 | _This project is not endorsed or supported by Proton Technologies AG._ 6 | 7 | # Install 8 | ``` 9 | npm install protonmail-api 10 | ``` 11 | 12 | # Tutorials 13 | In the process of adding more tutorials. For now another place to look is the [integration tests](https://github.com/justinkalland/protonmail-api/tree/master/tests/integration) 14 | 15 | - {@tutorial conversations} 16 | - {@tutorial emails} 17 | - {@tutorial move-inbox-to-spam-for-viagra} 18 | - {@tutorial folders} 19 | - {@tutorial create-folder} 20 | - {@tutorial delete-folder} 21 | - {@tutorial move} 22 | - {@tutorial labels} 23 | - {@tutorial create-label} 24 | - {@tutorial delete-label} 25 | - {@tutorial tag-untag} 26 | - {@tutorial send-email} -------------------------------------------------------------------------------- /.jsdoc.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "tags": { 4 | "allowUnknownTags": false 5 | }, 6 | "opts": { 7 | "template": "node_modules/docdash", 8 | "encoding": "utf8", 9 | "destination": "docs", 10 | "recurse": true, 11 | "verbose": true, 12 | "tutorials": "tutorials/" 13 | }, 14 | "source": { 15 | "include": [ 16 | "DOCS_README.md", 17 | "lib" 18 | ] 19 | }, 20 | "templates": { 21 | "default": { 22 | "outputSourceFiles": false, 23 | "includeDate": false 24 | } 25 | }, 26 | "docdash": { 27 | "sort": true, 28 | "sectionOrder": [ 29 | "Tutorials", 30 | "Classes", 31 | "Modules", 32 | "Externals", 33 | "Events", 34 | "Namespaces", 35 | "Mixins", 36 | "Interfaces" 37 | ], 38 | "menu":{ 39 | "GitHub ":{ 40 | "href":"https://github.com/justinkalland/protonmail-api", 41 | "class":"menu-item", 42 | "id":"source_link" 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /lib/folder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @hideconstructor 3 | */ 4 | class Folder { 5 | constructor (protonMail, data) { 6 | this._protonMail = protonMail 7 | /** 8 | * The unique identifier of the folder. 9 | * @type {string} 10 | */ 11 | this.id = String(data.ID) 12 | 13 | /** 14 | * The name of the folder. 15 | * @type {string} 16 | */ 17 | this.name = data.Name 18 | 19 | /** 20 | * True if a protected sysyem folder (such as inbox). 21 | * @type {boolean} 22 | */ 23 | this.isProtected = data.isProtected === true 24 | } 25 | 26 | /** 27 | * Permanently delete - this does not delete associated messages/conversations. 28 | */ 29 | async delete () { 30 | await this._protonMail._page.evaluate(id => { 31 | return window.labelModel.remove(id) 32 | }, this.id) 33 | 34 | this._protonMail.folders = this._protonMail.folders.filter(folder => { 35 | return folder.id !== this.id 36 | }) 37 | } 38 | } 39 | 40 | module.exports = Folder 41 | -------------------------------------------------------------------------------- /tutorials/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "send-email": { 3 | "title": "Send Email" 4 | }, 5 | "labels": { 6 | "title": "Labels", 7 | "children": { 8 | "create-label": { 9 | "title": "Create a New Label" 10 | }, 11 | "delete-label": { 12 | "title": "Delete a Label" 13 | }, 14 | "tag-untag": { 15 | "title": "Add and Remove Labels on Emails/Conversations" 16 | } 17 | } 18 | }, 19 | "folders": { 20 | "title": "Folders", 21 | "children": { 22 | "create-folder": { 23 | "title": "Create a New Folder" 24 | }, 25 | "delete-folder": { 26 | "title": "Delete a Folder" 27 | }, 28 | "move": { 29 | "title": "Move an Email or Conversation" 30 | } 31 | } 32 | }, 33 | "emails": { 34 | "title": "Emails", 35 | "children": { 36 | "move-inbox-to-spam-for-viagra": { 37 | "title": "Move emails from inbox to spam if they contain the word 'viagra'" 38 | } 39 | } 40 | }, 41 | "conversations": { 42 | "title": "Conversations" 43 | } 44 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Justin Kalland 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tutorials/labels/tag-untag.md: -------------------------------------------------------------------------------- 1 | ```js 2 | const ProtonMail = require('protonmail-api'); 3 | 4 | (async () => { 5 | const pm = await ProtonMail.connect({ 6 | username: 'foobar@protonmail.com', 7 | password: 'somethingsecure' 8 | }) 9 | 10 | // Get the first email in the inbox 11 | const emailsInInbox = await pm.getEmails('inbox') 12 | const email = emailsInInbox[0] 13 | 14 | // Tag the email with a label 15 | await email.addLabel('foo bar') 16 | console.log(`The email now has ${email.labels.length} labels`) 17 | 18 | // Untag the email 19 | await email.removeLabel('foo bar') 20 | console.log(`The email now has ${email.labels.length} labels`) 21 | 22 | pm.close() 23 | })() 24 | ``` 25 | 26 | Adding and removing labels from conversations works the same way, with the same method names: 27 | 28 | ```js 29 | const conversationsInInbox = await pm.getConversations('inbox') 30 | const conversation = conversationsInInbox[0] 31 | await conversation.addLabel('foo bar') 32 | await conversation.removeLavbel('foo bar') 33 | ``` 34 | 35 | ### Further resources: 36 | - {@link Label|Label class} 37 | - {@link Email|Email class} 38 | - {@link Conversation|Conversation class} -------------------------------------------------------------------------------- /docs/styles/prettify.css: -------------------------------------------------------------------------------- 1 | .pln { 2 | color: #ddd; 3 | } 4 | 5 | /* string content */ 6 | .str { 7 | color: #61ce3c; 8 | } 9 | 10 | /* a keyword */ 11 | .kwd { 12 | color: #fbde2d; 13 | } 14 | 15 | /* a comment */ 16 | .com { 17 | color: #aeaeae; 18 | } 19 | 20 | /* a type name */ 21 | .typ { 22 | color: #8da6ce; 23 | } 24 | 25 | /* a literal value */ 26 | .lit { 27 | color: #fbde2d; 28 | } 29 | 30 | /* punctuation */ 31 | .pun { 32 | color: #ddd; 33 | } 34 | 35 | /* lisp open bracket */ 36 | .opn { 37 | color: #000000; 38 | } 39 | 40 | /* lisp close bracket */ 41 | .clo { 42 | color: #000000; 43 | } 44 | 45 | /* a markup tag name */ 46 | .tag { 47 | color: #8da6ce; 48 | } 49 | 50 | /* a markup attribute name */ 51 | .atn { 52 | color: #fbde2d; 53 | } 54 | 55 | /* a markup attribute value */ 56 | .atv { 57 | color: #ddd; 58 | } 59 | 60 | /* a declaration */ 61 | .dec { 62 | color: #EF5050; 63 | } 64 | 65 | /* a variable name */ 66 | .var { 67 | color: #c82829; 68 | } 69 | 70 | /* a function name */ 71 | .fun { 72 | color: #4271ae; 73 | } 74 | 75 | /* Specify class=linenums on a pre to get line numbering */ 76 | ol.linenums { 77 | margin-top: 0; 78 | margin-bottom: 0; 79 | } 80 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [2.3.0](https://github.com/justinkalland/protonmail-api/compare/v2.2.0...v2.3.0) (2021-08-03) 6 | 7 | 8 | ### Features 9 | 10 | * add Types for TypeScript ([#82](https://github.com/justinkalland/protonmail-api/issues/82)) ([a032ebc](https://github.com/justinkalland/protonmail-api/commit/a032ebc2e1b89486dabfefb923ae2cbe4ba97f12)) 11 | 12 | ## [2.2.0](https://github.com/justinkalland/protonmail-api/compare/v2.1.1...v2.2.0) (2021-06-17) 13 | 14 | 15 | ### Features 16 | 17 | * add config.puppeteerOpts ([1678686](https://github.com/justinkalland/protonmail-api/commit/16786866028587b01c5956e590d9d949fcab33a6)) 18 | 19 | ### [2.1.1](https://github.com/justinkalland/protonmail-api/compare/v2.1.0...v2.1.1) (2021-06-15) 20 | 21 | 22 | ### Bug Fixes 23 | 24 | * throw error on invalid username and/or password ([8a3b831](https://github.com/justinkalland/protonmail-api/commit/8a3b8314fb47311532938589e3d3808f8a60b2af)), closes [#9](https://github.com/justinkalland/protonmail-api/issues/9) 25 | * use old.protonmail.com ([ea172ab](https://github.com/justinkalland/protonmail-api/commit/ea172ab0db0fcedad375c9821bfb0567f7845d90)) 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "protonmail-api", 3 | "version": "2.3.0", 4 | "description": "Node.js API for ProtonMail", 5 | "repository": "github:justinkalland/protonmail-api", 6 | "homepage": "https://github.com/justinkalland/protonmail-api#readme", 7 | "bugs": { 8 | "url": "https://github.com/justinkalland/protonmail-api/issues" 9 | }, 10 | "author": "Justin Kalland ", 11 | "license": "MIT", 12 | "types": "./types.d.ts", 13 | "scripts": { 14 | "test": "mocha tests --recursive --timeout 30000", 15 | "lint": "eslint . || true", 16 | "jsdoc": "jsdoc -c .jsdoc.json", 17 | "validate": "run-s test lint", 18 | "prerelease": "git checkout master && git pull origin master && run-s validate jsdoc", 19 | "release": "standard-version", 20 | "prepublishOnly": "npm run test" 21 | }, 22 | "dependencies": { 23 | "puppeteer": "^10.0.0" 24 | }, 25 | "devDependencies": { 26 | "@commitlint/cli": "^12.1.4", 27 | "@commitlint/config-conventional": "^12.1.4", 28 | "chai": "^4.3.4", 29 | "docdash": "github:clenemt/docdash", 30 | "dotenv": "^10.0.0", 31 | "eslint-config-jk": "^1.6.0", 32 | "jsdoc": "^3.6.4", 33 | "mocha": "^9.0.0", 34 | "npm-run-all": "^4.1.5", 35 | "standard-version": "^9.0.0" 36 | }, 37 | "husky": { 38 | "hooks": { 39 | "pre-commit": "lint-staged", 40 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 41 | } 42 | }, 43 | "lint-staged": { 44 | "*.js": "eslint" 45 | }, 46 | "eslintConfig": { 47 | "extends": "jk" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/integration/folder.test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect 2 | const support = require('./support') 3 | const Folder = require('../../lib/folder') 4 | 5 | describe('Folder', () => { 6 | it('returns folders', () => { 7 | const folders = support.pm.folders 8 | 9 | expect(folders).to.be.a('array') 10 | expect(folders[0]).to.be.an.instanceOf(Folder) 11 | }) 12 | 13 | it('creates', async () => { 14 | const name = 'TEST-' + support.randomString() 15 | const folder = await support.pm.createFolder(name) 16 | 17 | expect(folder).to.be.an.instanceOf(Folder) 18 | expect(folder.name).to.equal(name) 19 | 20 | await folder.delete() 21 | }) 22 | 23 | it('returns existing when creating duplicate', async () => { 24 | const name = 'TEST-' + support.randomString() 25 | const folder = await support.pm.createFolder(name) 26 | const folder2 = await support.pm.createFolder(name) 27 | 28 | expect(folder).to.equal(folder2) 29 | 30 | await folder.delete() 31 | }) 32 | 33 | it('deletes', async () => { 34 | const name = 'TEST-' + support.randomString() 35 | const folder = await support.pm.createFolder(name) 36 | 37 | expect(folder).to.be.an.instanceOf(Folder) 38 | 39 | await folder.delete() 40 | const lookupFolder = support.pm.getFolderByName(name) 41 | 42 | expect(lookupFolder).to.equal(undefined) 43 | }) 44 | 45 | it('gets by id', async () => { 46 | const name = 'TEST-' + support.randomString() 47 | const folder = await support.pm.createFolder(name) 48 | 49 | const lookupFolder = support.pm.getFolderById(folder.id) 50 | 51 | expect(lookupFolder).to.be.an.instanceOf(Folder) 52 | expect(lookupFolder.id).to.equal(folder.id) 53 | 54 | await folder.delete() 55 | }) 56 | 57 | it('gets by name', async () => { 58 | const name = 'TEST-' + support.randomString() 59 | const folder = await support.pm.createFolder(name) 60 | 61 | const lookupFolder = support.pm.getFolderByName(name) 62 | 63 | expect(lookupFolder).to.be.an.instanceOf(Folder) 64 | expect(folder.name).to.equal(name) 65 | 66 | await folder.delete() 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /tests/integration/label.test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect 2 | const support = require('./support') 3 | const Label = require('../../lib/label') 4 | 5 | describe('Label', () => { 6 | it('returns labels', async () => { 7 | const name = 'TEST-' + support.randomString() 8 | const label = await support.pm.createLabel(name) 9 | const labels = support.pm.labels 10 | 11 | expect(labels).to.be.a('array') 12 | expect(labels[0]).to.be.an.instanceOf(Label) 13 | 14 | await label.delete() 15 | }) 16 | 17 | it('creates', async () => { 18 | const name = 'TEST-' + support.randomString() 19 | const label = await support.pm.createLabel(name) 20 | 21 | expect(label).to.be.an.instanceOf(Label) 22 | expect(label.name).to.equal(name) 23 | 24 | await label.delete() 25 | }) 26 | 27 | it('returns existing when creating duplicate', async () => { 28 | const name = 'TEST-' + support.randomString() 29 | const label = await support.pm.createLabel(name) 30 | const label2 = await support.pm.createLabel(name) 31 | 32 | expect(label).to.equal(label2) 33 | 34 | await label.delete() 35 | }) 36 | 37 | it('deletes', async () => { 38 | const name = 'TEST-' + support.randomString() 39 | const label = await support.pm.createLabel(name) 40 | 41 | expect(label).to.be.an.instanceOf(Label) 42 | 43 | await label.delete() 44 | const lookupLabel = support.pm.getLabelByName(name) 45 | 46 | expect(lookupLabel).to.equal(undefined) 47 | }) 48 | 49 | it('gets by id', async () => { 50 | const name = 'TEST-' + support.randomString() 51 | const label = await support.pm.createLabel(name) 52 | 53 | const lookupLabel = support.pm.getLabelById(label.id) 54 | 55 | expect(lookupLabel).to.be.an.instanceOf(Label) 56 | expect(lookupLabel.id).to.equal(label.id) 57 | 58 | await label.delete() 59 | }) 60 | 61 | it('gets by name', async () => { 62 | const name = 'TEST-' + support.randomString() 63 | const label = await support.pm.createLabel(name) 64 | 65 | const lookupLabel = support.pm.getLabelByName(name) 66 | 67 | expect(lookupLabel).to.be.an.instanceOf(Label) 68 | expect(label.name).to.equal(name) 69 | 70 | await label.delete() 71 | }) 72 | }) 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # protonmail-api 2 | Unofficial API for interacting with ProtonMail. 3 | 4 | Allows interaction with ProtonMail through a simple Node.js API. Leverages the official [WebClient](https://github.com/ProtonMail/WebClient), keeping with the spirit of security and privacy. Currently supports sending email, managing email/conversations, and managing labels and folders. See the [documentation](https://justinkalland.github.io/protonmail-api/) for full functionality. 5 | 6 | _This project is not endorsed or supported by Proton Technologies AG._ 7 | 8 | # Quick Start 9 | ## Setup 10 | ``` 11 | npm install protonmail-api 12 | ``` 13 | ## Send an Email 14 | ```js 15 | const ProtonMail = require('protonmail-api'); 16 | 17 | (async () => { 18 | const pm = await ProtonMail.connect({ 19 | username: 'foobar@protonmail.com', 20 | password: 'somethingsecure' 21 | }) 22 | 23 | await pm.sendEmail({ 24 | to: 'justin@kalland.ch', 25 | subject: 'Send email tutorial', 26 | body: 'Hello world' 27 | }) 28 | 29 | pm.close() 30 | })() 31 | ``` 32 | ## More Examples 33 | Numerous examples can be found in the [tutorials section of the documentation](https://justinkalland.github.io/protonmail-api/). 34 | 35 | # Documentation 36 | [Full documentation found here](https://justinkalland.github.io/protonmail-api/) 37 | 38 | # How? 39 | This library uses [Puppeteer](https://github.com/GoogleChrome/puppeteer) (headless Chromium) to load and control the official [ProtonMail WebClient](https://github.com/ProtonMail/WebClient). 40 | 41 | The first attempt at building this was by trying to reverse engineer the API from the WebClient. This proved to be difficult and fragile. By utilizing the AngularJS modules (through the headless browser) this library is able to leverage all the work that goes into the official WebClient. This also means complex and sensitive things (like cryptography) are not handled by this library. The main drawback to this approach is the added weight of Puppeteer. 42 | 43 | # Contributing 44 | This project is looking for maintainers and contributors. Please contact justin@kalland.ch if you are interested. 45 | 46 | To run integration tests you need to provide a ProtonMail account. It is best to use a dedicated testing account without any filters. The credentials are set as environment variables `PM_USERNAME` and `PM_PASSWORD`. 47 | 48 | Example: 49 | ``` 50 | PM_USERNAME=footest@protonmail.com PM_PASSWORD=kgjSOE223qWer npm run test 51 | ``` 52 | 53 | Or create a `.env` file (in this directory next to `package.json`: 54 | ``` 55 | PM_USERNAME=footest@protonmail.com 56 | PM_PASSWORD=kgjSOE223qWer 57 | ``` 58 | -------------------------------------------------------------------------------- /docs/scripts/search.js: -------------------------------------------------------------------------------- 1 | 2 | var searchAttr = 'data-search-mode'; 3 | function contains(a,m){ 4 | return (a.textContent || a.innerText || "").toUpperCase().indexOf(m) !== -1; 5 | }; 6 | 7 | //on search 8 | document.getElementById("nav-search").addEventListener("keyup", function(event) { 9 | var search = this.value.toUpperCase(); 10 | 11 | if (!search) { 12 | //no search, show all results 13 | document.documentElement.removeAttribute(searchAttr); 14 | 15 | document.querySelectorAll("nav > ul > li:not(.level-hide)").forEach(function(elem) { 16 | elem.style.display = "block"; 17 | }); 18 | 19 | if (typeof hideAllButCurrent === "function"){ 20 | //let's do what ever collapse wants to do 21 | hideAllButCurrent(); 22 | } else { 23 | //menu by default should be opened 24 | document.querySelectorAll("nav > ul > li > ul li").forEach(function(elem) { 25 | elem.style.display = "block"; 26 | }); 27 | } 28 | } else { 29 | //we are searching 30 | document.documentElement.setAttribute(searchAttr, ''); 31 | 32 | //show all parents 33 | document.querySelectorAll("nav > ul > li").forEach(function(elem) { 34 | elem.style.display = "block"; 35 | }); 36 | //hide all results 37 | document.querySelectorAll("nav > ul > li > ul li").forEach(function(elem) { 38 | elem.style.display = "none"; 39 | }); 40 | //show results matching filter 41 | document.querySelectorAll("nav > ul > li > ul a").forEach(function(elem) { 42 | if (!contains(elem.parentNode, search)) { 43 | return; 44 | } 45 | elem.parentNode.style.display = "block"; 46 | }); 47 | //hide parents without children 48 | document.querySelectorAll("nav > ul > li").forEach(function(parent) { 49 | var countSearchA = 0; 50 | parent.querySelectorAll("a").forEach(function(elem) { 51 | if (contains(elem, search)) { 52 | countSearchA++; 53 | } 54 | }); 55 | 56 | var countUl = 0; 57 | var countUlVisible = 0; 58 | parent.querySelectorAll("ul").forEach(function(ulP) { 59 | // count all elements that match the search 60 | if (contains(ulP, search)) { 61 | countUl++; 62 | } 63 | 64 | // count all visible elements 65 | var children = ulP.children 66 | for (i=0; i; 31 | folders: Record; 32 | } 33 | 34 | export interface Conversation { 35 | id: string; 36 | subject: string; 37 | time: Date; 38 | isStarred: boolean; 39 | labels: Label[]; 40 | folder: Folder; 41 | 42 | getEmails: () => Promise; 43 | delete: () => Promise; 44 | move: (folder: Folder) => Promise; 45 | addLabel: (label: Label | string) => Promise; 46 | removeLabel: (label: Label | string) => Promise; 47 | star: () => Promise; 48 | unstar: () => Promise; 49 | } 50 | 51 | export interface Email { 52 | id: string; 53 | conversationId: string; 54 | subject: string; 55 | time: Date; 56 | from: Address; 57 | to: Address[]; 58 | cc: Address[]; 59 | bcc: Address[]; 60 | headers: Record; 61 | isStarred: boolean; 62 | isRead: boolean; 63 | labels: Label[]; 64 | folder: Folder; 65 | 66 | getBody: () => Promise; 67 | delete: () => Promise; 68 | move: (folder: Folder) => Promise; 69 | addLabel: (label: Label | string) => Promise; 70 | removeLabel: (label: Label | string) => Promise; 71 | star: () => Promise; 72 | unstar: () => Promise; 73 | getConversation: () => Promise; 74 | read: () => Promise; 75 | unread: () => Promise; 76 | } 77 | 78 | export interface Address { 79 | email: string; 80 | name: string; 81 | } 82 | 83 | export interface SendEmailOptions { 84 | to: Address | string; 85 | subject: string; 86 | body: string; 87 | } 88 | 89 | export interface ProtonMail { 90 | close: () => Promise; 91 | 92 | getLabelById: (id: string) => Label | undefined; 93 | 94 | getLabelByName: (name: string) => Label | undefined; 95 | 96 | getFolderById: (id: string) => Folder | undefined; 97 | 98 | getFolderByName: (name: string) => Folder | undefined; 99 | 100 | createFolder: (name: string) => Promise; 101 | 102 | createLabel: (name: string) => Promise