├── screenshots ├── email-ui.png └── inbound-screenshot-no-plugin.png ├── src ├── index.js ├── components │ ├── MarkdownMessageBubble │ │ ├── MarkdownMessageBubble.Styles.js │ │ └── MarkdownMessageBubble.jsx │ └── CustomMessageInput │ │ ├── CustomMessageInput.jsx │ │ └── CustomMessageInput.Styles.js └── EmailPlugin.js ├── server ├── .env.example ├── package.json ├── cleanup.js ├── utils.js ├── extractHtmlContent.js ├── index.js └── package-lock.json ├── public ├── plugins.json ├── plugins.local.build.json ├── appConfig.example.js └── index.html ├── craco.config.js ├── .gitignore ├── package.json └── README.md /screenshots/email-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-professional-services/plugin-email/HEAD/screenshots/email-ui.png -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import * as FlexPlugin from 'flex-plugin'; 2 | import EmailPlugin from './EmailPlugin'; 3 | 4 | FlexPlugin.loadPlugin(EmailPlugin); 5 | -------------------------------------------------------------------------------- /screenshots/inbound-screenshot-no-plugin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-professional-services/plugin-email/HEAD/screenshots/inbound-screenshot-no-plugin.png -------------------------------------------------------------------------------- /server/.env.example: -------------------------------------------------------------------------------- 1 | PORT=8080 2 | ACCOUNT_SID= 3 | AUTH_TOKEN= 4 | PROGRAMABLE_CHAT_SERVICE= 5 | WORKSPACE_SID= 6 | WORKFLOW_SID= 7 | FLEX_FLOW_SID= 8 | SG_MAIL_KEY= 9 | FROM_ADDRESS= 10 | -------------------------------------------------------------------------------- /public/plugins.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "plugin-email", 4 | "version": "0.0.0", 5 | "class": "EmailPlugin", 6 | "requires": [ 7 | { 8 | "@twilio/flex-ui": "^1" 9 | } 10 | ], 11 | "src": "http://localhost:3000/plugin-email.js" 12 | } 13 | ] -------------------------------------------------------------------------------- /public/plugins.local.build.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "EmailPlugin", 4 | "version": "0.0.0", 5 | "class": "EmailPlugin", 6 | "requires": [ 7 | { 8 | "@twilio/flex-ui": "^1" 9 | } 10 | ], 11 | "src": "http://127.0.0.1:8085" 12 | } 13 | ] 14 | -------------------------------------------------------------------------------- /public/appConfig.example.js: -------------------------------------------------------------------------------- 1 | // your account sid 2 | var accountSid = 'accountSid'; 3 | 4 | // set to /plugins.json for local dev 5 | // set to /plugins.local.build.json for testing your build 6 | // set to "" for the default live plugin loader 7 | var pluginServiceUrl = '/plugins.json'; 8 | 9 | var appConfig = { 10 | pluginService: { 11 | enabled: true, 12 | url: pluginServiceUrl, 13 | }, 14 | sso: { 15 | accountSid: accountSid 16 | }, 17 | ytica: false, 18 | logLevel: 'debug', 19 | showSupervisorDesktopView: true, 20 | }; 21 | -------------------------------------------------------------------------------- /craco.config.js: -------------------------------------------------------------------------------- 1 | const config = require('craco-config-flex-plugin'); 2 | 3 | module.exports = { 4 | ...config, 5 | plugins: [ 6 | // Customize app configuration (such as webpack, devServer, linter, etc) by creating a Craco plugin. 7 | // See https://github.com/sharegate/craco/tree/master/packages/craco#develop-a-plugin for more detail. 8 | // 9 | // Please note that Craco plugins have nothing to do with Flex plugins; it's just a naming coincidence. 10 | // Changes to this file are optional, you will not need to modify it for normal Flex Plugin development. 11 | ] 12 | }; 13 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twilio-flex-email-server", 3 | "version": "1.0.0", 4 | "description": "demo server for ingesting Twilio SendGrid e-mails and sending to Twilio Flex", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Martin Amps", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@sendgrid/mail": "^7.2.2", 13 | "axios": "^0.19.2", 14 | "body-parser": "^1.19.0", 15 | "cheerio": "^1.0.0-rc.3", 16 | "dotenv": "^8.2.0", 17 | "email-addresses": "^3.1.0", 18 | "express": "^4.17.1", 19 | "multer": "^1.4.2", 20 | "parse-headers": "^2.0.3", 21 | "showdown": "^1.9.1", 22 | "string": "^3.3.3", 23 | "turndown": "^6.0.0", 24 | "twilio": "^3.48.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /server/cleanup.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | const client = require('twilio')(process.env.ACCOUNT_SID, process.env.AUTH_TOKEN); 4 | 5 | console.log('cleaning up taskrouter tasks'); 6 | 7 | client.taskrouter.workspaces(process.env.WORKSPACE_SID) 8 | .tasks 9 | .list({limit: 500}) 10 | .then(tasks => { 11 | tasks.forEach(task => { 12 | task.remove().then(() => console.log('removed', task.sid)); 13 | }); 14 | }).catch(e => console.error('failed to cleanup tasks', e)); 15 | 16 | console.log('cleaning up chat channels'); 17 | 18 | client.chat.services(process.env.PROGRAMABLE_CHAT_SERVICE) 19 | .channels 20 | .list({limit: 500}) 21 | .then(channels => { 22 | channels.forEach(channel => { 23 | channel.remove().then(() => console.log('removed', channel.sid)); 24 | }); 25 | }).catch(e => console.error('failed to cleanup chat', e)); 26 | 27 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Twilio Flex 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | %FPM_JS_SCRIPTS% 20 | 21 | 22 | 23 | 24 |
25 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # Flex related ignore 64 | appConfig.js 65 | pluginsService.js 66 | build/ 67 | server/node_modules 68 | .DS_Store 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plugin-email", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "bootstrap": "flex-plugin check-start", 7 | "prebuild": "rimraf build && npm run bootstrap", 8 | "build": "flex-plugin build", 9 | "clear": "flex-plugin clear", 10 | "predeploy": "npm run build", 11 | "deploy": "flex-plugin deploy", 12 | "eject": "flex-plugin eject", 13 | "info": "flex-plugin info", 14 | "postinstall": "npm run bootstrap", 15 | "list": "flex-plugin list", 16 | "remove": "flex-plugin remove", 17 | "prestart": "npm run bootstrap", 18 | "start": "flex-plugin start", 19 | "test": "flex-plugin test --env=jsdom" 20 | }, 21 | "dependencies": { 22 | "craco-config-flex-plugin": "^3.13.5", 23 | "draft-js": "^0.11.6", 24 | "draftjs-to-markdown": "^0.6.0", 25 | "flex-plugin": "^3.13.2", 26 | "flex-plugin-scripts": "^3.13.5", 27 | "react": "16.5.2", 28 | "react-dom": "16.5.2", 29 | "react-draft-wysiwyg": "^1.14.5", 30 | "react-emotion": "9.2.6", 31 | "react-markdown": "^4.3.1", 32 | "react-scripts": "3.4.1" 33 | }, 34 | "devDependencies": { 35 | "@twilio/flex-ui": "^1", 36 | "babel-polyfill": "^6.26.0", 37 | "enzyme": "^3.10.0", 38 | "enzyme-adapter-react-16": "^1.14.0", 39 | "rimraf": "^3.0.0" 40 | }, 41 | "browserslist": { 42 | "production": [ 43 | ">0.2%", 44 | "not dead", 45 | "not op_mini all" 46 | ], 47 | "development": [ 48 | "last 1 chrome version", 49 | "last 1 firefox version", 50 | "last 1 safari version" 51 | ] 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/components/MarkdownMessageBubble/MarkdownMessageBubble.Styles.js: -------------------------------------------------------------------------------- 1 | import { default as styled } from 'react-emotion'; 2 | 3 | export default styled('div')` 4 | .messageToolbox a { 5 | display:inline-block; 6 | } 7 | 8 | .messageToolbox a:first-child { 9 | margin-right: 20px; 10 | } 11 | 12 | .markdownWrapper { 13 | overflow: hidden; 14 | text-overflow: hidden; 15 | margin-bottom: 15px; 16 | 17 | img { 18 | width: auto; 19 | height: auto; 20 | } 21 | } 22 | 23 | .bubbleWrapper { 24 | padding: 8px; 25 | } 26 | 27 | .bubbleHeader { 28 | display: flex; 29 | flex-wrap: nowrap; 30 | -webkit-box-flex: 1; 31 | flex-grow: 1; 32 | flex-shrink: 1; 33 | flex-direction: row; 34 | 35 | } 36 | 37 | .messageAuthor { 38 | margin-left: auto; 39 | margin-right: auto; 40 | font-size: 12px; 41 | width: 100%; 42 | display: flex; 43 | flex-wrap: nowrap; 44 | -webkit-box-flex: 1; 45 | flex-grow: 1; 46 | flex-shrink: 1; 47 | flex-direction: column; 48 | margin-right: 8px; 49 | } 50 | 51 | .messageDate { 52 | display:flex; 53 | text-align:right; 54 | font-size: 10px; 55 | flex: 0 0 auto; 56 | } 57 | 58 | .messageToolbox { 59 | display: flex; 60 | flex-wrap: nowrap; 61 | -webkit-box-flex: 1; 62 | flex-grow: 1; 63 | flex-shrink: 1; 64 | flex-direction: row; 65 | 66 | button:first-child { 67 | display:flex; 68 | flex: 0 1 auto; 69 | margin-right: 8px; 70 | } 71 | 72 | button { 73 | outline: none; 74 | display:flex; 75 | text-align:right; 76 | font-size: 10px; 77 | flex: 0 0 auto; 78 | } 79 | } 80 | 81 | 82 | strong { 83 | font-weight:bold; 84 | } 85 | `; 86 | -------------------------------------------------------------------------------- /src/components/CustomMessageInput/CustomMessageInput.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'react-emotion'; 3 | import { Editor } from 'react-draft-wysiwyg'; 4 | import { EditorState, convertToRaw } from 'draft-js'; 5 | import draftToMarkdown from 'draftjs-to-markdown'; 6 | import CustomMessageInputStyles from './CustomMessageInput.Styles' 7 | import * as Flex from "@twilio/flex-ui"; 8 | import { withTheme } from '@twilio/flex-ui'; 9 | 10 | const Button = styled(Flex.Button)` 11 | background-color: ${(props) => props.theme.colors.defaultButtonColor}; 12 | color: #fff; 13 | font-weight:bold; 14 | 15 | &:enabled { 16 | &:hover, 17 | &:active, 18 | &:focus { 19 | background-color: ${(props) => props.theme.colors.focusColor}; 20 | } 21 | } 22 | `; 23 | 24 | class CustomMessageInput extends React.Component { 25 | constructor(props) { 26 | super(props) 27 | this.props = props; 28 | this.state = { 29 | editorState: EditorState.createEmpty(), 30 | } 31 | } 32 | 33 | onEditorStateChange = (editorState) => { 34 | this.setState({ 35 | editorState 36 | }); 37 | } 38 | 39 | send = () => { 40 | const { channel, useSeparateInputStore } = this.props; 41 | 42 | const raw = convertToRaw(this.state.editorState.getCurrentContent()); 43 | const body = draftToMarkdown(raw); 44 | 45 | Flex.Actions.invokeAction('SendMessage', { 46 | body, 47 | channel, 48 | useSeparateInputStore 49 | }); 50 | 51 | this.setState({editorState: EditorState.createEmpty()}); 52 | } 53 | 54 | render() { 55 | return ( 56 | 57 | 70 | 71 | 72 | ); 73 | } 74 | } 75 | 76 | export default withTheme(CustomMessageInput); 77 | -------------------------------------------------------------------------------- /src/components/MarkdownMessageBubble/MarkdownMessageBubble.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactMarkdown from 'react-markdown'; 3 | import MarkdownMessageBubbleStyles from './MarkdownMessageBubble.Styles' 4 | import { Utils, withTheme } from '@twilio/flex-ui'; 5 | 6 | const MAX_HEIGHT = '175'; 7 | 8 | class MessageBubble extends React.Component { 9 | constructor(props) { 10 | super(props) 11 | this.props = props; 12 | this.state = { 13 | maximized: false, 14 | showButton: false 15 | } 16 | } 17 | 18 | componentDidMount() { 19 | this.scrollBottom(); 20 | this.loaded = false; 21 | } 22 | 23 | scrollBottom() { 24 | setTimeout(() => { 25 | const el = document.getElementsByClassName('Twilio-MessageList')[0]; 26 | el.scrollTop = el.clientHeight + 200; 27 | }, 50); 28 | } 29 | 30 | componentDidUpdate = () => { 31 | if (!this.loaded) { 32 | const imgs = this.markdownWrapper.querySelectorAll('img'); 33 | if (imgs.length) { 34 | imgs.forEach(i => { 35 | i.onload = this.maybeShowMoreButton; 36 | }); 37 | } 38 | } 39 | } 40 | 41 | maybeShowMoreButton = () => { 42 | if (this.markdownWrapper.clientHeight >= MAX_HEIGHT 43 | && !this.state.showButton) { 44 | this.setState({ 45 | showButton: true 46 | }); 47 | } 48 | } 49 | 50 | render() { 51 | const { member, useFriendlyName, authorName, message } = this.props; 52 | const name = Utils.getNameForMember(useFriendlyName, authorName, member.friendlyName, message.source.author); 53 | 54 | return ( 55 | 56 |
57 |
58 |
{name}
59 |
{message.source.timestamp.toLocaleTimeString([], { 60 | hour: '2-digit', 61 | minute: '2-digit' 62 | })} 63 |
64 |
65 | 66 |
this.markdownWrapper = el} 68 | style={{ maxHeight: this.state.maximized ? '' : MAX_HEIGHT + 'px' }} 69 | > 70 | 71 |
72 | 73 |
74 | {this.state.showButton 75 | ? 76 | : '' 77 | } 78 |
79 |
80 |
81 | ); 82 | } 83 | 84 | onToggleShow = () => { 85 | this.setState(prevState => ({ 86 | maximized: !prevState.maximized 87 | })); 88 | 89 | this.scrollBottom(); 90 | } 91 | } 92 | 93 | export default withTheme(MessageBubble); 94 | -------------------------------------------------------------------------------- /src/EmailPlugin.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FlexPlugin } from 'flex-plugin'; 3 | import { TaskHelper } from '@twilio/flex-ui'; 4 | 5 | import MarkdownMessageBubble from './components/MarkdownMessageBubble/MarkdownMessageBubble'; 6 | import CustomMessageInput from './components/CustomMessageInput/CustomMessageInput'; 7 | 8 | const PLUGIN_NAME = 'EmailPlugin'; 9 | 10 | export default class EmailPlugin extends FlexPlugin { 11 | constructor() { 12 | super(PLUGIN_NAME); 13 | } 14 | 15 | /** 16 | * This code is run when your plugin is being started 17 | * Use this to modify any UI components or attach to the actions framework 18 | * 19 | * @param flex { typeof import('@twilio/flex-ui') } 20 | * @param manager { import('@twilio/flex-ui').Manager } 21 | */ 22 | init(flex, manager) { 23 | this.registerEmailChannel(flex, manager); 24 | 25 | flex.MessageBubble.Content.replace(, { 26 | if: props => { 27 | const task = TaskHelper.getTaskFromChannelSid(props.message.source.channel.sid); 28 | return task && task.taskChannelUniqueName === 'email'; 29 | } 30 | }); 31 | 32 | flex.MessageInput.Content.replace(, { 33 | if: props => { 34 | let isEmail = false; 35 | try { 36 | const task = TaskHelper.getTaskFromChannelSid(props.channelSid); 37 | isEmail = task && task.taskChannelUniqueName === 'email' 38 | 39 | if (isEmail) { 40 | // TODO: Make less gross 41 | document.getElementsByClassName('Twilio-TaskCanvasHeader-Name')[0] 42 | .textContent = props.channel.source.attributes.pre_engagement_data.subject; 43 | 44 | document.querySelectorAll('.Twilio-TaskCanvasHeader-EndButton span')[0].textContent = 'END EMAIL INTERACTION'; 45 | document.querySelectorAll('.Twilio-TabHeader span')[0].textContent = 'Email'; 46 | } 47 | } catch (e) { 48 | console.error('failed to do things', e); 49 | } 50 | 51 | return isEmail; 52 | } 53 | }); 54 | 55 | // Add attributes and channel to task info panel, useful for debugging. 56 | manager.strings.TaskInfoPanelContent = manager.strings.TaskInfoPanelContent 57 | + `

Task Channel

{{task.taskChannelUniqueName}}

` 58 | + `

Attributes