├── .babelrc ├── .dockerignore ├── .eslintrc.js ├── .github └── FUNDING.yml ├── .gitignore ├── .prettierrc.js ├── .vscode └── launch.json ├── Dockerfile ├── Dockerfile-local ├── LICENSE.md ├── README.md ├── TODO.md ├── alias-config.js ├── components ├── FirebaseAuth.js ├── admin │ └── email │ │ └── fns │ │ ├── fullPath.js │ │ ├── generateKeyFromName.js │ │ └── generateUniqueId.js ├── common │ ├── Atoms.js │ └── common-header.js ├── ft │ ├── attachment-unlocker │ │ ├── AttachmentUnlocker.js │ │ ├── action-bar.js │ │ ├── config-ui.js │ │ └── email-preview.js │ └── email-json │ │ ├── EmailJson.js │ │ ├── ExtractionRules.js │ │ ├── Grid.js │ │ ├── config-ui.js │ │ └── rules-preview.js ├── pageWrappers │ ├── withAuthUser.js │ └── withAuthUserInfo.js └── service-creator │ ├── action-bar.js │ ├── config-output-bar.js │ ├── configuration-editor.js │ ├── email-preview.js │ ├── email-results-nav.js │ └── header.js ├── css ├── react-responsive-modal-override.css └── tailwind.css ├── docker-compose-dev.yml ├── docker-compose-local.yml ├── docker-compose.yml ├── next.config.js ├── package.json ├── pages ├── [uid] │ ├── dashboard │ │ └── index.js │ ├── ft │ │ ├── attachment-unlocker │ │ │ ├── [id].js │ │ │ └── index.js │ │ └── email-json │ │ │ ├── [id].js │ │ │ └── index.js │ └── home.js ├── _app.js ├── _document.js ├── api │ ├── apps │ │ ├── auto-unlock │ │ │ ├── index.js │ │ │ └── webhook.js │ │ └── email-to-json │ │ │ ├── index.js │ │ │ ├── integrations │ │ │ ├── google-sheet.js │ │ │ ├── sms.js │ │ │ └── whatsapp.js │ │ │ └── webhook.js │ ├── cron │ │ ├── cleanup-accounts.js │ │ ├── index.js │ │ └── user-cron.js │ ├── dashboard │ │ └── get-services.js │ ├── email-search │ │ ├── attachment-unlock.js │ │ └── index.js │ ├── fetch │ │ ├── attachment.js │ │ ├── email.js │ │ ├── preview-attachment-rules.js │ │ └── tables-from-attachment.js │ ├── firebase │ │ ├── associate-user.js │ │ ├── exchange-token.js │ │ ├── login.js │ │ ├── logout.js │ │ └── user.js │ ├── integrations │ │ ├── airtable │ │ │ └── index.js │ │ ├── google-spreadsheet │ │ │ ├── index.js │ │ │ └── preview.js │ │ ├── twilio │ │ │ └── index.js │ │ └── whatsapp │ │ │ ├── [path].js │ │ │ ├── checker.js │ │ │ ├── index.js │ │ │ └── subscriber.js │ ├── jsonbox │ │ ├── get-user.js │ │ └── put-user.js │ ├── otp │ │ └── index.js │ ├── user │ │ └── delete.js │ └── webhooks │ │ ├── credit-cards │ │ ├── amex.js │ │ └── dcb.js │ │ ├── inloopwith │ │ └── tech.js │ │ └── zerodha-cn │ │ └── index.js ├── helpers │ └── iframe.js └── index.js ├── postcss.config.js ├── public ├── favicon.ico ├── static │ └── images │ │ ├── favicon.png │ │ ├── logo.svg │ │ └── progressive-disclosure-line@2x.png └── vercel.svg ├── src ├── apps │ └── utils.js ├── auth.js ├── firebase │ ├── firebaseAdmin.js │ ├── firebaseSessionHandler.js │ ├── hooks.js │ ├── initFirebase.js │ ├── logout.js │ └── user.js ├── gmail.js ├── hooks │ └── useLocalState.js ├── integrations │ ├── email │ │ └── mailgun.js │ ├── google-spreadsheet │ │ ├── reset-sheet.js │ │ └── sync.js │ └── utils │ │ └── index.js ├── isomorphic │ ├── applyConfigOnEmail.js │ └── ensureConfiguration.js ├── middleware │ ├── commonMiddleware.js │ ├── cookieSession.js │ ├── cookieSessionRefresh.js │ └── ensureAuth.js ├── pdf │ ├── create-file.js │ ├── enums.js │ ├── extract-tables.js │ ├── templates │ │ ├── zerodha-cn-old.js │ │ └── zerodha-cn.js │ └── utils.js ├── queify.js ├── queues │ ├── auto-unlock.js │ ├── email-to-json.js │ ├── gsheet-sync.js │ ├── index.js │ ├── mail-fetch.js │ ├── notifications.js │ ├── pdf-to-json.js │ ├── send-email.js │ ├── send-whatsapp.js │ └── task-status.js ├── redis-queue.js ├── sentry.js └── utils.js ├── styles ├── Home.module.css └── globals.css ├── tailwind.config.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"], 3 | "plugins": [["styled-components", { "ssr": true }]] 4 | } 5 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | .vscode -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | node: true, 6 | }, 7 | parser: 'babel-eslint', 8 | extends: ['airbnb', 'prettier', 'prettier/react'], 9 | globals: { 10 | Atomics: 'readonly', 11 | SharedArrayBuffer: 'readonly', 12 | }, 13 | parserOptions: { 14 | ecmaFeatures: { 15 | jsx: true, 16 | modules: true, 17 | }, 18 | ecmaVersion: 2018, 19 | sourceType: 'module', 20 | }, 21 | plugins: ['react', 'prettier'], 22 | rules: { 23 | 'prettier/prettier': 'error', 24 | 'react/jsx-filename-extension': [1, { extensions: ['.js', '.jsx'] }], 25 | 'react/forbid-prop-types': [0, { forbid: ['any'] }], 26 | 'react/prop-types': 0, 27 | 'no-console': 0, 28 | 'no-debugger': 0, 29 | 'no-nested-ternary': 0, 30 | 'no-underscore-dangle': 0, 31 | 'global-require': 0, 32 | 'react/no-array-index-key': 0, 33 | 'jsx-a11y/no-static-element-interactions': 0, 34 | 'jsx-a11y/click-events-have-key-events': 0, 35 | }, 36 | settings: { 37 | 'import/resolver': { 38 | webpack: { 39 | config: './webpack.config.js', 40 | }, 41 | }, 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: aakashgoel 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ['buymeacoffee.com/aakashgoel', 'paypal.me/aakashlpin'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | .env.docker.local 33 | 34 | # vercel 35 | .vercel 36 | google-service-account.json 37 | 38 | .vscode 39 | .do -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | trailingComma: 'all', 4 | }; -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "attach", 7 | "name": "Launch Program", 8 | "skipFiles": ["/**"], 9 | "port": 9229 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:18.04 2 | # https://rtfm.co.ua/en/docker-configure-tzdata-and-timezone-during-build/ 3 | ENV TZ=Asia/Kolkata 4 | ENV LANG C.UTF-8 5 | ENV LC_ALL C.UTF-8 6 | RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone 7 | RUN apt-get update -y 8 | 9 | # Install Node.js 10 | RUN apt-get install -y curl 11 | RUN apt-get install -y gnupg-agent 12 | RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - 13 | RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list 14 | RUN curl --silent --location https://deb.nodesource.com/setup_12.x | bash - 15 | RUN apt-get install -y nodejs 16 | RUN apt-get install -y build-essential 17 | RUN apt install -y yarn 18 | RUN node --version 19 | RUN yarn --version 20 | 21 | RUN apt-get install -y qpdf 22 | RUN apt-get install -y software-properties-common 23 | RUN add-apt-repository ppa:deadsnakes/ppa 24 | RUN apt-get update 25 | RUN apt-get install -y python3.8 26 | RUN python3 --version 27 | RUN apt-get install -y python3-tk ghostscript 28 | RUN gs -version 29 | RUN apt-get install -y python3-pip 30 | RUN apt-get install -y libsm6 libxext6 libxrender-dev 31 | RUN pip3 install 'opencv-python==4.2.0.34' 32 | RUN pip3 install "camelot-py[cv]" 33 | 34 | WORKDIR /codebase 35 | COPY package.json ./ 36 | COPY yarn.lock ./ 37 | RUN yarn 38 | COPY . ./ 39 | ARG NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN 40 | ARG NEXT_PUBLIC_FIREBASE_DATABASE_URL 41 | ARG FIREBASE_CLIENT_EMAIL 42 | ARG NEXT_PUBLIC_FIREBASE_PROJECT_ID 43 | ARG NEXT_PUBLIC_FIREBASE_PUBLIC_API_KEY 44 | ARG FIREBASE_PRIVATE_KEY 45 | ARG NEXT_PUBLIC_GOOGLE_CLIENT_ID 46 | ARG GOOGLE_CLIENT_SECRET 47 | ARG NEXT_PUBLIC_GOOGLE_OAUTH_REDIRECT_URI 48 | ARG NEXT_PUBLIC_EMAILAPI_DOMAIN 49 | ARG EMAILAPI_BASE_URL 50 | ARG JSONBOX_NETWORK_URL 51 | ARG MAILGUN_API_KEY 52 | ARG MAILGUN_DOMAIN 53 | ARG NEXT_PUBLIC_SENDING_EMAIL_ID 54 | ARG MAILGUN_API_BASE_URL 55 | ARG OTP_EMAILAPI_USER_ID 56 | ARG NODE_DEBUG 57 | ARG NODE_ENV 58 | ARG SENTRY_DSN 59 | ARG GOOGLE_SERVICE_ACCOUNT_EMAIL 60 | ARG GOOGLE_PRIVATE_KEY 61 | ARG REDISCLOUD_URL 62 | RUN yarn build 63 | EXPOSE 3000 64 | CMD ["yarn", "start"] -------------------------------------------------------------------------------- /Dockerfile-local: -------------------------------------------------------------------------------- 1 | FROM ubuntu:18.04 2 | # https://rtfm.co.ua/en/docker-configure-tzdata-and-timezone-during-build/ 3 | ENV TZ=Asia/Kolkata 4 | ENV LANG C.UTF-8 5 | ENV LC_ALL C.UTF-8 6 | RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone 7 | RUN apt-get update -y 8 | 9 | # Install Node.js 10 | RUN apt-get install -y curl 11 | RUN apt-get install -y gnupg-agent 12 | RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - 13 | RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list 14 | RUN curl --silent --location https://deb.nodesource.com/setup_12.x | bash - 15 | RUN apt-get install -y nodejs 16 | RUN apt-get install -y build-essential 17 | RUN apt install -y yarn 18 | RUN node --version 19 | RUN yarn --version 20 | 21 | RUN apt-get install -y qpdf 22 | RUN apt-get install -y software-properties-common 23 | RUN add-apt-repository ppa:deadsnakes/ppa 24 | RUN apt-get update 25 | RUN apt-get install -y python3.8 26 | RUN python3 --version 27 | RUN apt-get install -y python3-tk ghostscript 28 | RUN gs -version 29 | RUN apt-get install -y python3-pip 30 | RUN apt-get install -y libsm6 libxext6 libxrender-dev 31 | RUN pip3 install 'opencv-python==4.2.0.34' 32 | RUN pip3 install "camelot-py[cv]" 33 | 34 | WORKDIR /codebase 35 | COPY package.json ./ 36 | COPY yarn.lock ./ 37 | COPY . ./ 38 | COPY .env.docker.local ./.env.local 39 | RUN yarn && yarn build 40 | EXPOSE 3000 41 | CMD ["yarn", "start"] -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 EmailAPI 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODOs 2 | 3 | ## Feature: Auto PDF Unlock 4 | 5 | - After search query is entered and an email is selected, if the email contains a PDF, 6 | - Show an option which says "Enable automatic PDF unlocking?" 7 | - On click, show an input to accept PDF password 8 | - Do a test dry run for this specific messageId + attachmentId combination 9 | where we attempt to temporarily download and unlock PDF on server 10 | then send this user mail with same subject prefixed with [UNLOCKED] and attach unlocked PDF 11 | then delete the file on server 12 | - If unlocked pdf mail from m.emailapi.io arrives, show options to 13 | - unlock all previous emails and receive "unlocked" emails for all of them 14 | - or, unlock all emails going forward 15 | 16 | ## Tracking task completion between parent-child queue jobs 17 | 18 | - ### 1-many jobs 19 | mailFetch will always be the parent job 20 | mailFetch queue will spawn n jobs in child queue 21 | 22 | - ### Parent job completion user notifications 23 | 24 | 25 | ### TODOs 26 | - Remove `done` based callbacks from queue and move to Promise.resolve()s 27 | - Enable job success/failure notification system 28 | - Include all queues from filesystem instead of `require`ing them individually 29 | 30 | 31 | ## Extracting tables 32 | 33 | ### Scenarios 34 | 35 | > User knows this PDF template always contains X number of tables 36 | 1. User can reject tables as is 37 | 2. User can select tables as is 38 | 2.1. or/and add whitelist rules 39 | 2.1.1. grab a row only if it contains a cell where cell's value `==` or `contains` some value 40 | 2.2. or/and add blacklist rules 41 | 2.2.1. reject rows if it contains a cell where cell's value `==` or `contains` some value 42 | 43 | # Last Notes 11th Oct 2020 8.35PM 44 | - UI: Added feature to allow posting to different Google Sheets for each `extraction rule` 45 | - Backend: stopped accepting gSheetId as a global prop and made it part of each rule object 46 | - Next steps: 47 | 1. Data extraction seems funky as one sheet contains results from both `rules` 48 | 2. Google Sheets API is rate limiting 429 - culprit being /preview endpoint which creates a new spreadsheet instance per API call 49 | 3. Options to solve #2 - Create a Sheets Factory with references stored on a per sheet id level (in redis to make it serverless?) and reuse them 50 | 51 | BCCPG2423G -------------------------------------------------------------------------------- /alias-config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | '~': path.resolve(__dirname), 5 | }; 6 | -------------------------------------------------------------------------------- /components/FirebaseAuth.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import styled from 'styled-components'; 3 | import firebase from 'firebase/app'; 4 | import 'firebase/auth'; 5 | import GoogleLogin from 'react-google-login'; 6 | import axios from 'axios'; 7 | import initFirebase from '~/src/firebase/initFirebase'; 8 | 9 | const SignInWithGmail = styled.button` 10 | border: 2px solid #ffc107; 11 | padding: 4px 8px; 12 | opacity: ${(props) => (props.disabled ? 0.5 : 1)}; 13 | `; 14 | 15 | // Init the Firebase app. 16 | initFirebase(); 17 | 18 | const FirebaseAuth = ({ 19 | uid = null, 20 | GOOGLE_CLIENT_ID, 21 | buttonLabel = 'Continue with Google Mail', 22 | scope = 'profile email https://www.googleapis.com/auth/gmail.readonly', 23 | callback = () => { 24 | console.log('auth successful, but no callback supplied!'); 25 | }, 26 | }) => { 27 | // Do not SSR FirebaseUI, because it is not supported. 28 | // https://github.com/firebase/firebaseui-web/issues/213 29 | const [renderAuth, setRenderAuth] = useState(false); 30 | useEffect(() => { 31 | if (typeof window !== 'undefined') { 32 | setRenderAuth(true); 33 | } 34 | }, []); 35 | 36 | const [isLoading, setIsLoading] = useState(false); 37 | const [isLoggedIn, setIsLoggedIn] = useState(false); 38 | 39 | async function onGoogleSignIn({ code }) { 40 | setIsLoading(true); 41 | console.log('exchanging auth code for tokens...'); 42 | const response = await axios(`/api/firebase/exchange-token?token=${code}`); 43 | console.log('[done] exchanging auth code for tokens...'); 44 | const { id_token: idToken, refresh_token: refreshToken } = response.data; 45 | 46 | const credential = firebase.auth.GoogleAuthProvider.credential(idToken); 47 | 48 | console.log('signin into firebase...'); 49 | firebase 50 | .auth() 51 | .signInWithCredential(credential) 52 | .then(async () => { 53 | console.log('[done] signin into firebase...'); 54 | const { currentUser } = firebase.auth(); 55 | let dbUser; 56 | if (!uid) { 57 | console.log('setting up user in db...'); 58 | dbUser = await axios.post(`/api/firebase/user`, { 59 | refresh_token: refreshToken, 60 | firebase_uid: currentUser.uid, 61 | }); 62 | callback(null, dbUser.data._id); 63 | } else { 64 | // [TODO] if this users email/firebase_uid already exists, then 65 | // 1. discard/delete the current browser uid (and hence the orphan db user) 66 | // 1.1. associate this mailbox to existing user 67 | // 2. refresh the page with existing user's uid 68 | 69 | console.log('associating firebase user in with uid...'); 70 | console.log({ currentUser }); 71 | dbUser = await axios.post(`/api/firebase/associate-user`, { 72 | uid, 73 | email: currentUser.email, 74 | firebase_uid: currentUser.uid, 75 | refresh_token: refreshToken, 76 | }); 77 | callback(null, dbUser.data); 78 | } 79 | console.log('[done] setting up user in db...', dbUser.data); 80 | // eslint-disable-next-line no-underscore-dangle 81 | setIsLoading(false); 82 | setIsLoggedIn(true); 83 | }) 84 | .catch((e) => { 85 | console.log(e); 86 | callback(e, null); 87 | setIsLoading(false); 88 | setIsLoggedIn(false); 89 | }); 90 | } 91 | 92 | function onGoogleSignInFailure(args) { 93 | console.log(args); 94 | } 95 | 96 | return ( 97 | <> 98 | {renderAuth && !isLoading && !isLoggedIn ? ( 99 | ( 109 | 114 | {buttonLabel} 115 | 116 | )} 117 | /> 118 | ) : null} 119 | {isLoading ? ( 120 | 121 | {buttonLabel} 122 | 123 | ) : null} 124 | {!isLoading && isLoggedIn ? `` : null} 125 | 126 | ); 127 | }; 128 | 129 | export default FirebaseAuth; 130 | -------------------------------------------------------------------------------- /components/admin/email/fns/fullPath.js: -------------------------------------------------------------------------------- 1 | export default function fullPath(el, stopProps = {}) { 2 | const names = []; 3 | while ( 4 | el.parentNode && 5 | (stopProps.className 6 | ? !el.parentNode.classList.contains(stopProps.className) 7 | : true) 8 | ) { 9 | if (el.id) { 10 | names.unshift(`#${el.id}`); 11 | break; 12 | } else { 13 | // eslint-disable-next-line 14 | if (el == el.ownerDocument.documentElement) 15 | names.unshift(el.tagName.toLowerCase()); 16 | else { 17 | for ( 18 | // eslint-disable-next-line 19 | var c = 1, e = el; 20 | e.previousElementSibling; 21 | e = e.previousElementSibling, c++ // eslint-disable-line 22 | ); 23 | names.unshift(`${el.tagName.toLowerCase()}:nth-child(${c})`); // eslint-disable-line 24 | } 25 | el = el.parentNode; // eslint-disable-line 26 | } 27 | } 28 | return names.join(' > '); 29 | } 30 | -------------------------------------------------------------------------------- /components/admin/email/fns/generateKeyFromName.js: -------------------------------------------------------------------------------- 1 | export default function generateKeyFromName(name) { 2 | return name.trim().toLowerCase().replace(/\s/g, '_'); 3 | } 4 | -------------------------------------------------------------------------------- /components/admin/email/fns/generateUniqueId.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-bitwise */ 2 | function createUniqueID() { 3 | let dt = new Date().getTime(); 4 | const uuid = 'xxyxxxxxxyxxxxxyxxxx'.replace(/[xy]/g, (c) => { 5 | const r = (dt + Math.random() * 16) % 16 | 0; 6 | dt = Math.floor(dt / 16); 7 | return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16); 8 | }); 9 | return uuid; 10 | } 11 | 12 | function generateUniqueId(prefix = '') { 13 | if (prefix.length > 4) 14 | return new Error('prefix length cannot be more than 4'); 15 | return `${prefix}${createUniqueID()}`; 16 | } 17 | 18 | module.exports = generateUniqueId; 19 | -------------------------------------------------------------------------------- /components/common/Atoms.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Shadow = styled.a.attrs({ 4 | className: 'inline-block uppercase text-gray-800 py-1 px-4 text-sm', 5 | })` 6 | background-color: hsl(228, 100%, 92%); 7 | box-shadow: 4px 4px hsla(228, 100%, 92%, 0.8); 8 | cursor: pointer; 9 | `; 10 | 11 | export const Separator = styled.div` 12 | background-image: url(/static/images/progressive-disclosure-line@2x.png); 13 | background-repeat: repeat-x; 14 | background-position: 0; 15 | background-size: 32px; 16 | height: 12px; 17 | `; 18 | 19 | export const VisibilityIcon = styled.span.attrs({ 20 | className: 'inline-block mr-1', 21 | })` 22 | vertical-align: bottom; 23 | `; 24 | 25 | export const StockItemContainer = styled.div` 26 | padding: 12px 24px 16px; 27 | background: ${(props) => 28 | props.isSelected ? 'hsla(219, 79%, 66%, 1)' : 'hsla(189, 50%, 95%, 1)'}; 29 | border: 1px solid hsla(214, 34%, 82%, 0.15); 30 | box-shadow: 0 4px 0px hsl(214, 34%, 82%); 31 | border-radius: 4px; 32 | cursor: ${(props) => (props.isActionable ? 'pointer' : 'initial')}; 33 | `; 34 | 35 | export const StockItemBadge = styled.span` 36 | background: hsl(0, 0%, 86%); 37 | border-radius: 4px; 38 | padding: 2px 4px; 39 | text-align: center; 40 | color: hsl(0, 0%, 17%); 41 | border: 1px dashed hsl(0, 0%, 60%); 42 | `; 43 | 44 | export const Anchor = styled.a.attrs({ 45 | className: 'cursor-pointer', 46 | })` 47 | border-bottom: 5px solid #ffc107; 48 | `; 49 | 50 | export const Button = styled.button.attrs((props) => ({ 51 | type: props.type || 'button', 52 | }))` 53 | border-bottom: 5px solid #ffc107; 54 | opacity: ${(props) => (props.disabled ? 0.25 : 1)}; 55 | cursor: ${(props) => (props.disabled ? 'not-allowed' : 'pointer')}; 56 | 57 | &:focus { 58 | outline: none; 59 | } 60 | `; 61 | 62 | export const Label = styled.div.attrs({ 63 | className: 'uppercase text-gray-800 tracking-wide text-sm', 64 | })``; 65 | 66 | export const Value = styled.span``; 67 | 68 | export const FlexEnds = styled.div.attrs({ 69 | className: 'flex flex-1 w-full items-center justify-between', 70 | })``; 71 | 72 | export const Row = styled.div.attrs({ 73 | className: 'flex items-center', 74 | })` 75 | > * { 76 | margin-right: 4px; 77 | } 78 | `; 79 | -------------------------------------------------------------------------------- /components/common/common-header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withRouter } from 'next/router'; 3 | import styled from 'styled-components'; 4 | 5 | const Header = styled.header` 6 | display: grid; 7 | grid-template-columns: 400px 1fr 600px; 8 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); 9 | align-items: center; 10 | `; 11 | 12 | const Logo = styled.img` 13 | max-height: 24px; 14 | `; 15 | 16 | function CommonHeader({ children, router }) { 17 | return ( 18 |
19 |
20 | 21 |
22 |
{children}
23 |
24 | Dashboard 25 |
26 |
27 | ); 28 | } 29 | 30 | export default withRouter(CommonHeader); 31 | -------------------------------------------------------------------------------- /components/ft/attachment-unlocker/action-bar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { format } from 'date-fns'; 4 | 5 | import { Button, Label, FlexEnds } from '~/components/common/Atoms'; 6 | 7 | const Nudges = styled.div.attrs({ 8 | className: 'border-b', 9 | })` 10 | display: grid; 11 | grid-template-columns: 400px 1fr 600px; 12 | align-items: end; 13 | padding: 0.5rem; 14 | `; 15 | 16 | const ActionBar = ({ 17 | isLoading, 18 | searchResults, 19 | nextPageToken, 20 | handleFetchMoreMails, 21 | }) => ( 22 | 23 |
24 | 25 |
26 | 27 | {searchResults.length} /{' '} 28 | {searchResults.length 29 | ? format( 30 | new Date(searchResults[searchResults.length - 1].date), 31 | 'dd MMM yyyy', 32 | ) 33 | : 'Loading...'} 34 |
35 |
36 | 42 |
43 |
44 |
45 |
Original Email
46 |
47 | Attachment Unlocker Settings 48 |
49 |
50 | ); 51 | 52 | export default ActionBar; 53 | -------------------------------------------------------------------------------- /components/ft/attachment-unlocker/email-preview.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const normalizeHtmlWhitespace = require('normalize-html-whitespace'); 5 | 6 | const EmailContainer = styled.div.attrs({ 7 | className: 'pt-4 p-8 pb-16', 8 | })` 9 | overflow-y: scroll; 10 | `; 11 | 12 | const EmailPreview = ({ showPreview, messageItem }) => { 13 | if (!showPreview) { 14 | return null; 15 | } 16 | 17 | if (!messageItem) { 18 | return ( 19 | Select an email on the left to preview 20 | ); 21 | } 22 | 23 | const { isHtmlContent = true, message } = messageItem; 24 | 25 | return ( 26 | 27 |
/g, // remove global styles sent in email. styles are always inlined anyways 34 | '', 35 | ) 36 | .replace(//g, '') 37 | .replace(//g, ''), 38 | ) 39 | : `
${message}
`, 40 | }} 41 | onClick={(e) => e.preventDefault()} 42 | /> 43 | 44 | ); 45 | }; 46 | 47 | export default EmailPreview; 48 | -------------------------------------------------------------------------------- /components/ft/email-json/Grid.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-props-no-spreading */ 2 | import React from 'react'; 3 | import styled from 'styled-components'; 4 | import { toArray } from '../../../src/pdf/utils'; 5 | 6 | const StyledGrid = styled.div` 7 | display: grid; 8 | grid-template-columns: ${(props) => `repeat(${props.cols}, 1fr)`}; 9 | border: 1px solid #ddd; 10 | overflow-x: scroll; 11 | font-size: 0.8rem; 12 | max-width: calc(50vw - 4rem - 1rem); 13 | `; 14 | 15 | const GridRowBaseCell = styled.div.attrs((props) => ({ 16 | className: props.selected ? 'bg-orange-100' : null, 17 | }))` 18 | border-bottom: 1px solid #ddd; 19 | border-right: 1px solid #ddd; 20 | padding: 2px 4px; 21 | `; 22 | 23 | const GridRowClickableCell = styled(GridRowBaseCell)` 24 | cursor: pointer; 25 | &:hover { 26 | background: orange; 27 | border: 1px solid orange; 28 | color: #281a01; 29 | } 30 | `; 31 | 32 | const GridCell = ({ isCellClickable, ...props }) => 33 | isCellClickable ? ( 34 | 35 | ) : ( 36 | 37 | ); 38 | 39 | export default function Grid({ 40 | data, 41 | isCellClickable = false, 42 | cellClickCb = () => {}, 43 | className = '', 44 | selectedRows = [], 45 | selectedCells = [], 46 | }) { 47 | if (!data) { 48 | return

Glitch!

; 49 | } 50 | console.log({ selectedRows }); 51 | return ( 52 | 53 | {data.map((row, rowIdx) => { 54 | const cells = toArray(row); 55 | return ( 56 | <> 57 | {cells.map((cell, colIdx) => ( 58 | 65 | selectedCell.rowIdx === rowIdx && 66 | selectedCell.colIdx === colIdx, 67 | ) 68 | } 69 | onClick={() => 70 | isCellClickable 71 | ? cellClickCb({ value: cell, rowIdx, colIdx }) 72 | : null 73 | } 74 | > 75 | {cell} 76 | 77 | ))} 78 | 79 | ); 80 | })} 81 | 82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /components/ft/email-json/rules-preview.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { getRuleDataFromTable } from '~/src/pdf/utils'; 3 | import Grid from './Grid'; 4 | 5 | export default function RulePreview({ data, rule }) { 6 | // function validateCell 7 | const ruleDataFromTable = getRuleDataFromTable({ data, rule }); 8 | return ( 9 |
10 |

Rows Extracted by cell rules:

11 | 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /components/pageWrappers/withAuthUser.js: -------------------------------------------------------------------------------- 1 | /* eslint react/jsx-props-no-spreading: 0 */ 2 | 3 | import React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import { get, set } from 'lodash/object'; 6 | import { AuthUserInfoContext, useFirebaseAuth } from '~/src/firebase/hooks'; 7 | import { createAuthUser, createAuthUserInfo } from '~/src/firebase/user'; 8 | 9 | // Gets the authenticated user from the Firebase JS SDK, when client-side, 10 | // or from the request object, when server-side. Add the AuthUserInfo to 11 | // context. 12 | export default (ComposedComponent) => { 13 | const WithAuthUserComp = (props) => { 14 | const { AuthUserInfo, ...otherProps } = props; 15 | 16 | // We'll use the authed user from client-side auth (Firebase JS SDK) 17 | // when available. On the server side, we'll use the authed user from 18 | // the session. This allows us to server-render while also using Firebase's 19 | // client-side auth functionality. 20 | const { user: firebaseUser } = useFirebaseAuth(); 21 | const AuthUserFromClient = createAuthUser(firebaseUser); 22 | const { AuthUser: AuthUserFromSession, token, uid } = AuthUserInfo; 23 | const AuthUser = AuthUserFromClient || AuthUserFromSession || null; 24 | 25 | return ( 26 | 27 | 28 | 29 | ); 30 | }; 31 | 32 | WithAuthUserComp.getInitialProps = async (ctx) => { 33 | const { req, res } = ctx; 34 | 35 | // Get the AuthUserInfo object. 36 | let AuthUserInfo; 37 | if (typeof window === 'undefined') { 38 | // If server-side, get AuthUserInfo from the session in the request. 39 | // Don't include server middleware in the client JS bundle. See: 40 | // https://arunoda.me/blog/ssr-and-server-only-modules 41 | const { addSession } = require('~/src/middleware/cookieSession'); 42 | addSession(req, res); 43 | AuthUserInfo = createAuthUserInfo({ 44 | firebaseUser: get(req, 'session.decodedToken', null), 45 | token: get(req, 'session.token', null), 46 | uid: get(req, 'session.uid', null), 47 | }); 48 | } else { 49 | // If client-side, get AuthUserInfo from stored data. We store it 50 | // in _document.js. See: 51 | // https://github.com/zeit/next.js/issues/2252#issuecomment-353992669 52 | try { 53 | const jsonData = JSON.parse( 54 | window.document.getElementById('__MY_AUTH_USER_INFO').textContent, 55 | ); 56 | if (jsonData) { 57 | AuthUserInfo = jsonData; 58 | } else { 59 | // Use the default (unauthed) user info if there's no data. 60 | AuthUserInfo = createAuthUserInfo(); 61 | } 62 | } catch (e) { 63 | // If there's some error, use the default (unauthed) user info. 64 | AuthUserInfo = createAuthUserInfo(); 65 | } 66 | } 67 | 68 | // Explicitly add the user to a custom prop in the getInitialProps 69 | // context for ease of use in child components. 70 | set(ctx, 'myCustomData.AuthUserInfo', AuthUserInfo); 71 | 72 | // Evaluate the composed component's getInitialProps(). 73 | let composedInitialProps = {}; 74 | if (ComposedComponent.getInitialProps) { 75 | composedInitialProps = await ComposedComponent.getInitialProps(ctx); 76 | } 77 | 78 | return { 79 | ...composedInitialProps, 80 | AuthUserInfo, 81 | }; 82 | }; 83 | 84 | WithAuthUserComp.displayName = `WithAuthUser(${ComposedComponent.displayName})`; 85 | 86 | WithAuthUserComp.propTypes = { 87 | AuthUserInfo: PropTypes.shape({ 88 | AuthUser: PropTypes.shape({ 89 | id: PropTypes.string.isRequired, 90 | email: PropTypes.string.isRequired, 91 | emailVerified: PropTypes.bool.isRequired, 92 | }), 93 | token: PropTypes.string, 94 | }).isRequired, 95 | }; 96 | 97 | WithAuthUserComp.defaultProps = {}; 98 | 99 | return WithAuthUserComp; 100 | }; 101 | -------------------------------------------------------------------------------- /components/pageWrappers/withAuthUserInfo.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-props-no-spreading */ 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import { get } from 'lodash/object'; 5 | import { AuthUserInfoContext } from '~/src/firebase/hooks'; 6 | 7 | // Provides an AuthUserInfo prop to the composed component. 8 | export default (ComposedComponent) => { 9 | const WithAuthUserInfoComp = (props) => { 10 | const { AuthUserInfo: AuthUserInfoFromSession, ...otherProps } = props; 11 | return ( 12 | 13 | {(AuthUserInfo) => ( 14 | 18 | )} 19 | 20 | ); 21 | }; 22 | 23 | WithAuthUserInfoComp.getInitialProps = async (ctx) => { 24 | const AuthUserInfo = get(ctx, 'myCustomData.AuthUserInfo', null); 25 | 26 | // Evaluate the composed component's getInitialProps(). 27 | let composedInitialProps = {}; 28 | if (ComposedComponent.getInitialProps) { 29 | composedInitialProps = await ComposedComponent.getInitialProps(ctx); 30 | } 31 | 32 | return { 33 | ...composedInitialProps, 34 | AuthUserInfo, 35 | }; 36 | }; 37 | 38 | WithAuthUserInfoComp.displayName = `WithAuthUserInfo(${ComposedComponent.displayName})`; 39 | 40 | WithAuthUserInfoComp.propTypes = { 41 | // eslint-disable-next-line react/require-default-props 42 | AuthUserInfo: PropTypes.shape({ 43 | AuthUser: PropTypes.shape({ 44 | id: PropTypes.string.isRequired, 45 | email: PropTypes.string.isRequired, 46 | emailVerified: PropTypes.bool.isRequired, 47 | }), 48 | token: PropTypes.string, 49 | }), 50 | }; 51 | 52 | WithAuthUserInfoComp.defaultProps = {}; 53 | 54 | return WithAuthUserInfoComp; 55 | }; 56 | -------------------------------------------------------------------------------- /components/service-creator/action-bar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import flatten from 'lodash/flatten'; 4 | import { format } from 'date-fns'; 5 | 6 | import { Button, Label, FlexEnds } from '~/components/common/Atoms'; 7 | 8 | const Nudges = styled.div.attrs({ 9 | className: 'border-b', 10 | })` 11 | display: grid; 12 | grid-template-columns: 400px 1fr 600px; 13 | align-items: end; 14 | padding: 0.5rem; 15 | `; 16 | 17 | const ActionBar = ({ 18 | serviceId, 19 | isLoading, 20 | parsedData, 21 | searchResults, 22 | isPreviewMode, 23 | nextPageToken, 24 | onClickCreateAPI, 25 | isCreateApiPending, 26 | doPreviewParsedData, 27 | handleFetchMoreMails, 28 | handleClickSyncIntegrations, 29 | matchedSearchResults, 30 | }) => ( 31 | 32 |
33 | 34 |
35 | 36 | {searchResults.length} /{' '} 37 | {searchResults.length 38 | ? format( 39 | new Date(searchResults[searchResults.length - 1].date), 40 | 'dd MMM yyyy', 41 | ) 42 | : 'Loading...'} 43 |
44 |
45 | 51 |
52 |
53 |
54 |
Original Email
55 |
56 |
57 | {!serviceId ? ( 58 | <> 59 | 60 | {searchResults.length && matchedSearchResults.length 61 | ? `${parseInt( 62 | (matchedSearchResults.length / searchResults.length) * 100, 63 | 10, 64 | )}%` 65 | : '-'} 66 | 67 | ) : null} 68 |
69 | 70 | {/* */} 77 | 84 | 91 | 94 |
95 |
96 | ); 97 | 98 | export default ActionBar; 99 | -------------------------------------------------------------------------------- /components/service-creator/configuration-editor.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import generateKeyFromName from '~/components/admin/email/fns/generateKeyFromName'; 4 | import { Label } from '~/components/common/Atoms'; 5 | 6 | const Container = styled.div``; 7 | 8 | const FieldItem = styled.div.attrs({ 9 | className: '', 10 | })` 11 | padding: 1rem; 12 | border-bottom: 1px solid hsl(60, 69%, 79%); 13 | `; 14 | 15 | const Actions = styled.div` 16 | display: grid; 17 | grid-template-columns: 50% 50%; 18 | `; 19 | 20 | const ExtractedValue = styled.div.attrs({ 21 | className: 'text-xl', 22 | })` 23 | max-width: 90%; 24 | overflow: hidden; 25 | text-overflow: ellipsis; 26 | white-space: nowrap; 27 | `; 28 | 29 | const NameInput = styled.input.attrs({ 30 | className: 'border border-solid rounded border-gray-200 w-full', 31 | })``; 32 | 33 | const NameInputForm = styled.form` 34 | display: grid; 35 | grid-template-columns: 200px 1fr; 36 | grid-template-rows: 1fr; 37 | width: 100%; 38 | `; 39 | 40 | const ConfigurationEditor = ({ 41 | configuration, 42 | onChangeConfiguration, 43 | sampleData, 44 | localFieldNames, 45 | setLocalFieldName, 46 | }) => { 47 | function handleAssignNameForSelector(value, fieldKey) { 48 | onChangeConfiguration({ 49 | ...configuration, 50 | fields: configuration.fields.map((field) => 51 | field && field.fieldKey === fieldKey 52 | ? { 53 | ...field, 54 | fieldName: value, 55 | fieldKey: generateKeyFromName(value), 56 | } 57 | : field, 58 | ), 59 | }); 60 | } 61 | 62 | function handleAssignFormatterOnValue(formatter, fieldKey) { 63 | onChangeConfiguration( 64 | { 65 | ...configuration, 66 | fields: configuration.fields.map((field) => 67 | field && field.fieldKey === fieldKey 68 | ? { 69 | ...field, 70 | formatter, 71 | } 72 | : field, 73 | ), 74 | }, 75 | true, 76 | ); 77 | } 78 | 79 | function handleChangeGroupByParent(groupByParent, fieldKey) { 80 | onChangeConfiguration( 81 | { 82 | ...configuration, 83 | fields: configuration.fields.map((field) => 84 | field && field.fieldKey === fieldKey 85 | ? { 86 | ...field, 87 | groupByParent, 88 | } 89 | : field, 90 | ), 91 | }, 92 | true, 93 | ); 94 | } 95 | 96 | function onChangeFieldName({ value, fieldKey }) { 97 | setLocalFieldName({ 98 | ...localFieldNames, 99 | [fieldKey]: value, 100 | }); 101 | } 102 | 103 | function onSaveFieldName(fieldKey) { 104 | const updatedFieldName = localFieldNames[fieldKey]; 105 | handleAssignNameForSelector(updatedFieldName, fieldKey); 106 | } 107 | 108 | return ( 109 | 110 | {configuration.fields 111 | .filter((field) => field) 112 | .map((field) => { 113 | const { fieldKey, fieldName, selector, groupByParent } = field; 114 | const hasLocalName = typeof localFieldNames[fieldKey] === 'string'; 115 | return ( 116 | 117 |
118 | 119 | 120 | {Array.isArray(sampleData[fieldKey]) 121 | ? 'Values are grouped' 122 | : sampleData[fieldKey]} 123 | 124 |
125 | 126 |
127 | 128 | { 130 | e.preventDefault(); 131 | onSaveFieldName(fieldKey); 132 | }} 133 | > 134 | 139 | onChangeFieldName({ 140 | value: e.target.value, 141 | fieldKey, 142 | }) 143 | } 144 | onBlur={() => onSaveFieldName(fieldKey)} 145 | /> 146 | 147 |
148 |
149 | 150 | 168 |
169 |
170 |
171 | ); 172 | })} 173 |
174 | ); 175 | }; 176 | 177 | export default ConfigurationEditor; 178 | -------------------------------------------------------------------------------- /components/service-creator/email-preview.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import styled from 'styled-components'; 3 | import { Trash2 } from 'react-feather'; 4 | import { createPopper } from '@popperjs/core'; 5 | import fullPath from '~/components/admin/email/fns/fullPath'; 6 | import { Button } from '~/components/common/Atoms'; 7 | 8 | const normalizeHtmlWhitespace = require('normalize-html-whitespace'); 9 | 10 | const MailMessageContainer = styled.div.attrs({ 11 | className: 'border-r border-l', 12 | })` 13 | overflow-y: scroll; 14 | `; 15 | 16 | const Popover = styled.div` 17 | background: #fff; 18 | color: #642f45; 19 | padding: 2px 8px; 20 | border-radius: 2px; 21 | font-weight: bold; 22 | font-size: 14px; 23 | text-align: left; 24 | border: 2px solid #ffc107; 25 | `; 26 | 27 | const EmailPreview = ({ 28 | showPreview, 29 | messageItem, 30 | message, 31 | isHtmlContent = true, 32 | handleClickEmailContent, 33 | handleClickAttachmentFilename, 34 | onDeleteFieldFromConfiguration, 35 | configuration, 36 | isNotClickable, 37 | parsedData, 38 | }) => { 39 | function removeClassname(classname) { 40 | [...document.querySelectorAll(`.${classname}`)].forEach( 41 | (elem) => elem && elem.classList.remove(classname), 42 | ); 43 | } 44 | 45 | function resetHoverInMailContent() { 46 | removeClassname('mail-container-hover'); 47 | } 48 | 49 | function handleMouseLeaveInMailContent() { 50 | resetHoverInMailContent(); 51 | } 52 | 53 | function handleMouseMoveInMailContent(e) { 54 | resetHoverInMailContent(); 55 | document 56 | .querySelector(fullPath(e.target)) 57 | .classList.add('mail-container-hover'); 58 | } 59 | 60 | function onClickEmailContent(e) { 61 | e.preventDefault(); 62 | const stopPathAt = { 63 | className: 'gmail_quote', 64 | }; 65 | const clickedElemPathElems = fullPath(e.target, stopPathAt) 66 | .replace('#mailContainer > ', '') 67 | .split(' > '); 68 | 69 | let isFirstSelectorPathChanged = false; 70 | for (let i = 0; i < clickedElemPathElems.length; i++) { // eslint-disable-line 71 | if (isFirstSelectorPathChanged) { 72 | break; 73 | } 74 | if (clickedElemPathElems[i].includes(':nth-child(')) { 75 | // eslint-disable-next-line prefer-destructuring 76 | clickedElemPathElems[i] = clickedElemPathElems[i].split(':')[0]; 77 | isFirstSelectorPathChanged = true; 78 | } 79 | } 80 | 81 | const clickedElemSelector = clickedElemPathElems.join(' > '); 82 | 83 | handleClickEmailContent({ selector: clickedElemSelector, name: null }); 84 | } 85 | 86 | async function onClickFilename({ messageId, attachmentId, filename }) { 87 | handleClickAttachmentFilename({ messageId, attachmentId, filename }); 88 | } 89 | 90 | useEffect(() => { 91 | if (!configuration) { 92 | return; 93 | } 94 | removeClassname('eaio-field'); 95 | configuration.fields 96 | .filter((field) => field) 97 | .forEach((field) => { 98 | if (field.groupByParent) { 99 | console.log({ field, parsedData }); 100 | } 101 | const documentSel = document.querySelector(`${field.selector}`); 102 | if (!documentSel) { 103 | return; 104 | } 105 | const classesAtSelector = documentSel.classList; 106 | 107 | if (!classesAtSelector.contains('eaio-field')) { 108 | classesAtSelector.add('eaio-field'); 109 | const tooltip = document.querySelector( 110 | `#eaio-popver-${field.fieldKey}`, 111 | ); 112 | createPopper(documentSel, tooltip, { 113 | placement: 'top', 114 | }); 115 | } 116 | }); 117 | }); 118 | 119 | if (!showPreview) { 120 | return null; 121 | } 122 | 123 | if (!message) { 124 | return
Select an email on the left to preview
; 125 | } 126 | 127 | const interactions = !isNotClickable || !isHtmlContent; 128 | 129 | return ( 130 | 131 | {messageItem && Array.isArray(messageItem.attachments) ? ( 132 |
133 | {messageItem.attachments.map((attachment) => ( 134 |
135 | 146 |
147 | ))} 148 |
149 | ) : null} 150 |
/g, // remove global styles sent in email. styles are always inlined anyways 158 | '', 159 | ) 160 | .replace(//g, '') 161 | .replace(//g, ''), 162 | ) 163 | : `
${message}
`, 164 | }} 165 | onClick={interactions ? onClickEmailContent : (e) => e.preventDefault()} 166 | onMouseLeave={interactions ? handleMouseLeaveInMailContent : null} 167 | onMouseMove={interactions ? handleMouseMoveInMailContent : null} 168 | /> 169 | {configuration 170 | ? configuration.fields 171 | .filter((field) => field) 172 | .map((field) => { 173 | return ( 174 | 178 | {interactions ? ( 179 | 188 | ) : null} 189 | 190 | {field.fieldName} 191 | 192 | ); 193 | }) 194 | : null} 195 | 196 | ); 197 | }; 198 | 199 | export default EmailPreview; 200 | -------------------------------------------------------------------------------- /components/service-creator/email-results-nav.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import cx from 'classnames'; 4 | import { format } from 'date-fns'; 5 | import { Filter } from 'react-feather'; 6 | 7 | const MailUIWrapper = styled.div.attrs({ 8 | className: 'border-b', 9 | })` 10 | height: inherit; 11 | `; 12 | 13 | const MailSearchResults = styled.div.attrs({ 14 | className: '', 15 | })` 16 | height: inherit; 17 | `; 18 | 19 | const Nav = styled.nav.attrs({ 20 | className: '', 21 | })` 22 | height: 100%; 23 | overflow-y: scroll; 24 | `; 25 | 26 | const EmailResultsNav = ({ 27 | isLoading, 28 | isServiceIdFetched, 29 | searchResults, 30 | matchedSearchResults = [], 31 | selectedSearchResultIndex, 32 | handleClickEmailSubject, 33 | handleFilterEmailsBySender, 34 | handleFilterEmailsBySubject, 35 | }) => ( 36 | 112 | ); 113 | 114 | export default EmailResultsNav; 115 | -------------------------------------------------------------------------------- /components/service-creator/header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import CommonHeader from '~/components/common/common-header'; 4 | 5 | const InputSearch = styled.input.attrs({ 6 | className: 'w-full px-4 py-2 border border-gray-400 border-solid rounded', 7 | })` 8 | opacity: ${(props) => (props.disabled ? 0.4 : 1)}; 9 | `; 10 | 11 | const Header = ({ 12 | serviceId, 13 | searchInput, 14 | setTriggerSearch, 15 | isLoading, 16 | isServiceIdFetched, 17 | handleChangeSearchInput, 18 | }) => ( 19 | 20 | {serviceId ? ( 21 | searchInput ? ( 22 |
“{searchInput}”
23 | ) : null 24 | ) : ( 25 |
{ 28 | e.preventDefault(); 29 | setTriggerSearch(true); 30 | }} 31 | > 32 | handleChangeSearchInput(e.target.value)} 37 | placeholder="Search query from Google..." 38 | /> 39 | 40 | )} 41 |
42 | ); 43 | 44 | export default Header; 45 | -------------------------------------------------------------------------------- /css/react-responsive-modal-override.css: -------------------------------------------------------------------------------- 1 | .react-responsive-modal-overlay { 2 | padding: 0; 3 | } 4 | 5 | .react-responsive-modal-modal { 6 | width: 100%; 7 | max-width: calc(100% - 4rem); 8 | padding: 0; 9 | } -------------------------------------------------------------------------------- /css/tailwind.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss/base"; 2 | @import "tailwindcss/components"; 3 | @import "tailwindcss/utilities"; 4 | -------------------------------------------------------------------------------- /docker-compose-dev.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | jsonbox_dev: 5 | container_name: jsonbox_dev 6 | image: 'docker.pkg.github.com/aakashlpin/jsonbox/jsonbox:latest' 7 | env_file: 8 | - ~/apps/jsonbox/dev.env 9 | ports: 10 | - '4001:3000' 11 | 12 | redis_dev: 13 | container_name: redis_dev 14 | image: 'bitnami/redis:latest' 15 | volumes: 16 | - /tmp/dev/redis/data:/bitnami/redis/data 17 | environment: 18 | - ALLOW_EMPTY_PASSWORD=yes 19 | 20 | emailapi_dev: 21 | container_name: emailapi_dev 22 | restart: always 23 | image: 'docker.pkg.github.com/aakashlpin/emailapi/emailapi:dev' 24 | env_file: 25 | - ~/apps/emailapi-pipeline/dev.env 26 | volumes: 27 | - /tmp:/tmp 28 | ports: 29 | - '4000:3000' 30 | depends_on: 31 | - jsonbox_dev 32 | - redis_dev 33 | -------------------------------------------------------------------------------- /docker-compose-local.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | jsonbox: 5 | container_name: jsonbox 6 | build: ~/clones/jsonbox/. 7 | env_file: 8 | - ~/apps/jsonbox/.env 9 | ports: 10 | - '3001:3000' 11 | 12 | redis: 13 | image: 'bitnami/redis:latest' 14 | environment: 15 | - ALLOW_EMPTY_PASSWORD=yes 16 | 17 | emailapi: 18 | container_name: emailapi 19 | restart: always 20 | build: . 21 | ports: 22 | - '3000:3000' 23 | depends_on: 24 | - jsonbox 25 | - redis 26 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | jsonbox: 5 | container_name: jsonbox 6 | image: 'docker.pkg.github.com/aakashlpin/jsonbox/jsonbox:latest' 7 | env_file: 8 | - ~/apps/jsonbox/.env 9 | ports: 10 | - '3001:3000' 11 | 12 | redis: 13 | image: 'bitnami/redis:latest' 14 | volumes: 15 | - /tmp/redis/data:/bitnami/redis/data 16 | environment: 17 | - ALLOW_EMPTY_PASSWORD=yes 18 | 19 | emailapi: 20 | container_name: emailapi 21 | restart: always 22 | image: 'docker.pkg.github.com/aakashlpin/emailapi/emailapi:latest' 23 | env_file: 24 | - ~/apps/emailapi-pipeline/docker.env 25 | volumes: 26 | - /tmp:/tmp 27 | ports: 28 | - '3000:3000' 29 | depends_on: 30 | - jsonbox 31 | - redis 32 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const nextEnv = require('next-env'); 2 | const withPlugins = require('next-compose-plugins'); 3 | const withCSS = require('@zeit/next-css'); 4 | const aliases = require('./alias-config'); 5 | 6 | const withNextEnv = nextEnv(); 7 | 8 | const nextConfig = { 9 | webpack: (config) => { 10 | const { alias } = config.resolve; 11 | config.resolve.alias = { // eslint-disable-line 12 | ...alias, 13 | ...aliases, 14 | }; 15 | return config; 16 | }, 17 | }; 18 | 19 | module.exports = withPlugins([withNextEnv, withCSS], nextConfig); 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "emailapi", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "NODE_OPTIONS='--inspect' next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "build_start": "next build && next start" 10 | }, 11 | "dependencies": { 12 | "@popperjs/core": "^2.4.4", 13 | "@sentry/node": "^5.22.3", 14 | "@sentry/tracing": "^5.22.3", 15 | "@zeit/next-css": "^1.0.1", 16 | "airtable": "^0.10.0", 17 | "autoprefixer": "^9.8.6", 18 | "axios": "^0.19.2", 19 | "base-64": "^0.1.0", 20 | "base64topdf": "^1.1.8", 21 | "bitly": "^7.1.0", 22 | "bluebird": "^3.7.2", 23 | "bull": "^3.18.0", 24 | "classnames": "^2.2.6", 25 | "cookie-session": "^1.4.0", 26 | "date-fns": "^2.15.0", 27 | "firebase": "^7.18.0", 28 | "firebase-admin": "^9.1.0", 29 | "follow-redirects": "^1.13.0", 30 | "get-urls": "^10.0.0", 31 | "google-spreadsheet": "^3.0.13", 32 | "googleapis": "^59.0.0", 33 | "ioredis": "^4.17.3", 34 | "isomorphic-unfetch": "^3.0.0", 35 | "jsdom": "^16.4.0", 36 | "json-formatter-js": "^2.3.4", 37 | "lodash": "^4.17.20", 38 | "mailgun-js": "^0.22.0", 39 | "next": "9.5.1", 40 | "next-compose-plugins": "^2.2.1", 41 | "next-env": "^1.1.1", 42 | "normalize-html-whitespace": "^1.0.0", 43 | "noty": "^3.2.0-beta", 44 | "postcss-import": "^12.0.1", 45 | "prop-types": "^15.7.2", 46 | "react": "16.13.1", 47 | "react-dom": "16.13.1", 48 | "react-feather": "^2.0.8", 49 | "react-google-login": "^5.1.21", 50 | "react-responsive-modal": "^5.0.3", 51 | "remove-pdf-password": "^0.1.0", 52 | "shelljs": "^0.8.4", 53 | "shortid": "^2.2.15", 54 | "styled-components": "^5.1.1", 55 | "tailwindcss": "^1.6.2", 56 | "twilio": "^3.50.0", 57 | "unzipper": "^0.10.11" 58 | }, 59 | "devDependencies": { 60 | "babel-eslint": "^10.1.0", 61 | "babel-plugin-styled-components": "^1.11.1", 62 | "eslint": "^7.7.0", 63 | "eslint-config-airbnb": "^18.2.0", 64 | "eslint-config-prettier": "^6.11.0", 65 | "eslint-import-resolver-webpack": "^0.12.2", 66 | "eslint-plugin-import": "^2.22.0", 67 | "eslint-plugin-jsx-a11y": "^6.3.1", 68 | "eslint-plugin-prettier": "^3.1.4", 69 | "eslint-plugin-react": "^7.20.6", 70 | "esm": "^3.2.25", 71 | "prettier": "^2.0.5" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /pages/[uid]/ft/attachment-unlocker/[id].js: -------------------------------------------------------------------------------- 1 | import AttachmentUnlocker from '~/components/ft/attachment-unlocker/AttachmentUnlocker'; 2 | 3 | export default AttachmentUnlocker; 4 | -------------------------------------------------------------------------------- /pages/[uid]/ft/attachment-unlocker/index.js: -------------------------------------------------------------------------------- 1 | import AttachmentUnlocker from '~/components/ft/attachment-unlocker/AttachmentUnlocker'; 2 | 3 | export default AttachmentUnlocker; 4 | -------------------------------------------------------------------------------- /pages/[uid]/ft/email-json/[id].js: -------------------------------------------------------------------------------- 1 | import EmailJson from '~/components/ft/email-json/EmailJson'; 2 | 3 | export default EmailJson; 4 | -------------------------------------------------------------------------------- /pages/[uid]/ft/email-json/index.js: -------------------------------------------------------------------------------- 1 | import EmailJson from '~/components/ft/email-json/EmailJson'; 2 | 3 | export default EmailJson; 4 | -------------------------------------------------------------------------------- /pages/[uid]/home.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import React from 'react'; 3 | import { withRouter } from 'next/router'; 4 | import Head from 'next/head'; 5 | import Link from 'next/link'; 6 | import styled from 'styled-components'; 7 | import CommonHeader from '~/components/common/common-header'; 8 | import withAuthUser from '~/components/pageWrappers/withAuthUser'; 9 | import withAuthUserInfo from '~/components/pageWrappers/withAuthUserInfo'; 10 | import { Anchor } from '~/components/common/Atoms'; 11 | 12 | const Container = styled.div` 13 | display: grid; 14 | grid-template-columns: 1fr; 15 | grid-template-rows: 64px 1fr; 16 | `; 17 | 18 | const Body = styled.div` 19 | padding: 1rem 0.5rem; 20 | `; 21 | 22 | const Dashboard = ({ router }) => { 23 | const { 24 | query: { uid }, 25 | } = router; 26 | 27 | return ( 28 | <> 29 | 30 | emailapi.io | create new job 31 | 32 | 33 | 34 | 35 |
36 | Goto Dashboard 37 |
38 |
39 | 40 | 41 |

Select job type

42 |
43 |

44 | 45 | PDF attachment Unlocker 46 | 47 |

48 |
49 | 50 |
51 |

52 | 53 | Email to JSON 54 | 55 |

56 |
57 | 58 |
59 | 60 | ); 61 | }; 62 | 63 | export default withAuthUser(withAuthUserInfo(withRouter(Dashboard))); 64 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import App from 'next/app'; 3 | import '../css/tailwind.css'; 4 | 5 | class EmailApiApp extends App { 6 | render() { 7 | const { Component, pageProps } = this.props; 8 | // eslint-disable-next-line react/jsx-props-no-spreading 9 | return ; 10 | } 11 | } 12 | 13 | export default EmailApiApp; 14 | -------------------------------------------------------------------------------- /pages/_document.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Document, { Html, Head, Main, NextScript } from 'next/document'; 3 | import { ServerStyleSheet } from 'styled-components'; 4 | 5 | class MyDocument extends Document { 6 | static async getInitialProps(ctx) { 7 | const sheet = new ServerStyleSheet(); 8 | const originalRenderPage = ctx.renderPage; 9 | 10 | try { 11 | ctx.renderPage = () => 12 | originalRenderPage({ 13 | enhanceApp: (App) => (props) => 14 | sheet.collectStyles(), 15 | }); 16 | 17 | const initialProps = await Document.getInitialProps(ctx); 18 | return { 19 | ...initialProps, 20 | styles: ( 21 | <> 22 | {initialProps.styles} 23 | {sheet.getStyleElement()} 24 | 25 | ), 26 | }; 27 | } finally { 28 | sheet.seal(); 29 | } 30 | } 31 | 32 | render() { 33 | return ( 34 | 35 | 36 | 37 | 38 | 39 | 43 | 44 | 48 | 54 | 55 | 56 | 60 |