Skip to content
This repository was archived by the owner on Jun 26, 2020. It is now read-only.

feat(unsubscribe): merge n1-unsubscribe plugin #49

Merged
merged 2 commits into from
Jul 30, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ Great starting points for creating your own plugins!
- [View on GitHub](https://github.com/nylas/nylas-mail/tree/master/internal_packages/message-view-on-github)
- [Personal Level Indicators](https://github.com/nylas/nylas-mail/tree/master/internal_packages/personal-level-indicators)
- [Phishing Detection](https://github.com/nylas/nylas-mail/tree/master/internal_packages/phishing-detection)
- [Unsubscribe](https://github.com/nylas/nylas-mail/tree/master/internal_packages/unsubscribe)

#### Community Plugins

Expand All @@ -96,7 +97,6 @@ Note these are not tested or officially supported by Nylas, but we still think t
- [Jiffy](http://noahbuscher.github.io/N1-Jiffy/)—Insert animated GIFs
- [Weather](https://github.com/jackiehluo/n1-weather)
- [Todoist](https://github.com/alexfruehwirth/N1TodoistIntegration)
- [Unsubscribe](https://github.com/colinking/n1-unsubscribe)
- [Squirt Speed Reader](https://github.com/HarleyKwyn/squirt-reader-N1-plugin/)
- [Website Launcher](https://github.com/adriangrantdotorg/nylas-n1-background-webpage)—Opens a URL in separate window
- In Development: [Cypher](https://github.com/mbilker/cypher) (PGP Encryption)
Expand Down
16 changes: 16 additions & 0 deletions packages/client-app/internal_packages/unsubscribe/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
![Unsubscribe: unsubscribe without leaving Nylas Mail](plugin.png)

Quickly unsubscribe from emails without leaving Nylas Mail. The unsubscribe plugin parses the `list-unsubscribe` header and the email body to look for the best way to unsubscribe. If an unsubscribe email address can be found, the plugin will send one in the background on your behalf. If only a browser link is found, either a mini Nylas Mail browser window will open or you will be redirected to your default browser based on your preferences and certain exceptions.

## Keyboard Shortcuts

Press <kbd>CMD</kbd> + <kbd>ALT</kbd> + <kbd>U</kbd> when viewing an email. This shortcut can be changed in the preference panel (`Nylas->Preferences->Unsubscribe`).

## Reporting Bugs

- **Feature Requests or Bug Reports**: Submit them through the [issues](issues) pane.
- **Mishandled Emails**: Find an email which this plugin doesn't handle correctly? Not finding an unsubscribe link when there should be one? Forward the email to us at <a href="mailto:[email protected]">[email protected]</a> and we'll look into it.

## Made by

[Kyle King](http://kyleking.me) and [Colin King](http://colinking.co)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"unsubscribe:unsubscribe": "mod+alt+u"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default {
LOADING: 'LOADING',
ERRORED: 'ERRORED',
READY: 'READY',
UNSUBSCRIBED: 'UNSUBSCRIBED',
};
51 changes: 51 additions & 0 deletions packages/client-app/internal_packages/unsubscribe/lib/main.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {ComponentRegistry, PreferencesUIStore} from 'nylas-exports';
import {ThreadUnsubscribeQuickActionButton, ThreadUnsubscribeToolbarButton}
from './ui/unsubscribe-buttons';
import UnsubscribePreferences from './ui/unsubscribe-preferences';

export const config = {
defaultBrowser: {
"title": "Default browser",
"type": "string",
"default": "popup",
"enum": ["popup", "native"],
'enumLabels': ["Popup Window", "Native Browser"],
},
handleThreads: {
"title": "Default unsubscribe behaivor",
"type": "string",
"default": "archive",
"enum": ["archive", "trash", "none"],
'enumLabels': ["Archive", "Trash", "None"],
},
confirmForEmail: {
"title": "Confirm before sending email-based unsubscribe requests",
"type": "boolean",
"default": false,
},
confirmForBrowser: {
"title": "Confirm before opening web-based unsubscribe links",
"type": "boolean",
"default": false,
},
}

export function activate() {
ComponentRegistry.register(ThreadUnsubscribeQuickActionButton,
{ role: 'ThreadListQuickAction' });
ComponentRegistry.register(ThreadUnsubscribeToolbarButton,
{ role: 'ThreadActionsToolbarButton' });

this.preferencesTab = new PreferencesUIStore.TabItem({
tabId: 'Unsubscribe',
displayName: "Unsubscribe",
component: UnsubscribePreferences,
});
PreferencesUIStore.registerPreferencesTab(this.preferencesTab);
}

export function deactivate() {
PreferencesUIStore.unregisterPreferencesTab(this.preferencesTab);
ComponentRegistry.unregister(ThreadUnsubscribeQuickActionButton);
ComponentRegistry.unregister(ThreadUnsubscribeToolbarButton);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import ThreadUnsubscribeStore from './thread-unsubscribe-store';

class ThreadUnsubscribeStoreManager {
constructor() {
this.threads = {};
}

getStoreForThread(thread) {
const id = thread.id;
if (this.threads[id] === undefined) {
this.threads[id] = new ThreadUnsubscribeStore(thread);
}
return this.threads[id];
}
}

export default new ThreadUnsubscribeStoreManager();
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import {
Actions,
TaskFactory,
FocusedPerspectiveStore,
NylasAPI,
NylasAPIRequest,
} from 'nylas-exports';
import NylasStore from 'nylas-store';
import {MailParser} from 'mailparser';
import {remote} from 'electron';
import open from 'open';
import _ from 'underscore';
import {logIfDebug, shortenURL, shortenEmail, interpretEmail, userConfirm} from './util/helpers';
import {electronCantOpen} from './util/blacklist';
import EmailParser from './util/email-parser';
import ThreadConditionType from './enum/threadConditionType';

export default class ThreadUnsubscribeStore extends NylasStore {
constructor(thread) {
super();
this.settings = NylasEnv.config.get("unsubscribe");

if (!thread) {
NylasEnv.reportError(new Error("Invalid thread object"));
this.threadState = {
id: null,
condition: ThreadConditionType.ERRORED,
}
} else {
this.thread = thread;
this.threadState = {
id: this.thread.id,
condition: ThreadConditionType.LOADING,
}
this.messages = this.thread.__messages;
this._loadLinks();
}
}

_triggerUpdate() {
this.trigger(this.threadState);
}

unsubscribe() {
if (this.parser && this.parser.canUnsubscribe()) {
const unsubscribeHandler = (error, unsubscribed) => {
if (error) {
this.threadState.condition = ThreadConditionType.ERRORED;
NylasEnv.reportError(error, this);
} else if (unsubscribed) {
this._moveThread();
this.threadState.condition = ThreadConditionType.UNSUBSCRIBED;
}
this._triggerUpdate();
};

if (this.parser.emails.length > 0) {
this._unsubscribeViaMail(this.parser.emails[0], unsubscribeHandler);
} else {
this._unsubscribeViaBrowser(this.parser.urls[0], unsubscribeHandler);
}
}
}

_loadLinks() {
this._loadMessagesViaAPI((error, email) => {
if (error) {
this.threadState.condition = ThreadConditionType.ERRORED;
NylasEnv.reportError(error, this);
} else if (email) {
const confirmText = "Are you sure that you want to unsubscribe?";
this.isForwarded = this.thread.subject.match(/^Fwd: /i);
this.confirmText = this.isForwarded ? `The email was forwarded. ${confirmText}` : confirmText;

this.parser = new EmailParser(email.headers, email.html, email.text);
this.threadState.condition = this.parser.canUnsubscribe() ? ThreadConditionType.READY : ThreadConditionType.DISABLED;
// Output troubleshooting info
logIfDebug(`Found ${(this.parser.canUnsubscribe() ? "" : "no ")}links for: "${this.thread.subject}"`);
logIfDebug(this.parser);
} else {
this.threadState.condition = ThreadConditionType.DISABLED;
}
this._triggerUpdate();
});
}

_loadMessagesViaAPI(callback) {
if (this.messages && this.messages.length > 0) {
if (this.messages[0].draft || (this.messages[0].categories &&
_.some(this.messages[0].categories, _.matcher({displayName: "Sent Mail"})))) {
// Can't unsubscribe from draft or sent emails
callback(null, null);
} else {
// Fetch the email contents to parse for unsubscribe links
// NOTE: This will only make a request for the first email message in the thread,
// instead of all messages based on the assumption that the first email will have
// an unsubscribe link iff you can unsubscribe from that thread.
const messagePath = `/messages/${this.messages[0].id}`;
new NylasAPIRequest({
api: NylasAPI,
options: {
accountId: this.thread.accountId,
path: messagePath,
headers: {Accept: "message/rfc822"},
json: false,
},
})
.run()
.then((rawEmail) => {
const mailparser = new MailParser();
mailparser.on('end', (parsedEmail) => {
callback(null, parsedEmail);
});
mailparser.write(rawEmail);
mailparser.end();
})
.catch((err) => {
callback(err)
});
}
} else {
callback(new Error('No messages found to parse for unsubscribe links.'));
}
}

_unsubscribeViaBrowser(url, callback) {
if ((!this.isForwarded && !this.settings.confirmForBrowser) ||
userConfirm(this.confirmText, `A browser will be opened at: ${shortenURL(url)}`)) {
logIfDebug(`Opening a browser window to:\n${url}`);
if (this.settings.defaultBrowser === "native" || electronCantOpen(url)) {
open(url);
callback(null, /* unsubscribed=*/true);
} else {
const browserWindow = new remote.BrowserWindow({
'web-preferences': { 'web-security': false, 'nodeIntegration': false },
'width': 1000,
'height': 800,
'center': true,
"alwaysOnTop": true,
});
browserWindow.on('closed', () => {
callback(null, /* unsubscribed=*/true);
});
browserWindow.webContents.on('did-fail-load', (event, errorCode, errorDescription) => {
// Unable to load this URL in a browser window. Redirect to a native browser.
logIfDebug(`Failed to open URL in browser window: ${errorCode} ${errorDescription}`);
browserWindow.destroy();
open(url);
});
browserWindow.loadURL(url);
browserWindow.show();
}
} else {
callback(null, /* unsubscribed=*/false);
}
}

_unsubscribeViaMail(emailAddress, callback) {
if (emailAddress) {
if ((!this.isForwarded && !this.settings.confirmForEmail) ||
userConfirm(this.confirmText, `An email will be sent to:\n${shortenEmail(emailAddress)}`)) {
logIfDebug(`Sending an email to: ${emailAddress}`);
new NylasAPIRequest({
api: NylasAPI,
options: {
accountId: this.thread.accountId,
path: '/send',
method: 'POST',
body: interpretEmail(emailAddress),
},
})
.run()
.catch((err) => {
NylasEnv.reportError(err, this)
});
// Send the callback now so that emails are moved immediately
// instead of waiting for the email to be sent.
callback(null, /* unsubscribed= */true);
} else {
callback(null, /* unsubscribed= */false);
}
} else {
callback(new Error(`Invalid email address (${emailAddress})`), /* unsubscribed= */false);
}
}

_moveThread() {
switch (this.settings.handleThreads) {
case "trash":
if (FocusedPerspectiveStore.current().canTrashThreads([this.thread])) {
const tasks = TaskFactory.tasksForMovingToTrash({
threads: [this.thread],
fromPerspective: FocusedPerspectiveStore.current(),
});
Actions.queueTasks(tasks);
}
break;
case "archive":
if (FocusedPerspectiveStore.current().canArchiveThreads([this.thread])) {
const tasks = TaskFactory.tasksForArchiving({
threads: [this.thread],
fromPerspective: FocusedPerspectiveStore.current(),
});
Actions.queueTasks(tasks);
}
break;
default:
// "none" case -- do not move email
}
Actions.popSheet();
}
}
Loading