├── .babelrc
├── .dockerignore
├── .env.example
├── .eslintrc
├── .gitignore
├── .osfg-dir-config.js
├── .sequelizerc
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE.md
├── README.md
├── client
├── actions
│ ├── accountsManagementActions.js
│ ├── appActions.js
│ ├── appActions.spec.js
│ ├── campaignActions.js
│ ├── campaignActions.spec.js
│ ├── listActions.js
│ ├── listActions.spec.js
│ ├── notificationActions.js
│ ├── permissionActions.js
│ ├── permissionActions.spec.js
│ ├── settingsActions.js
│ └── settingsActions.spec.js
├── components
│ ├── 404
│ │ ├── index.js
│ │ └── index.spec.js
│ ├── accountsManagement
│ │ ├── CreateAccountForm.js
│ │ └── DeleteAccountForm.js
│ ├── admin-lte
│ │ ├── Footer.js
│ │ ├── Footer.spec.js
│ │ ├── Header.js
│ │ ├── Header.spec.js
│ │ ├── RightSidebar.js
│ │ ├── RightSidebar.spec.js
│ │ ├── Sidebar.js
│ │ ├── Sidebar.spec.js
│ │ ├── WS-Notification.js
│ │ └── WS-Notification.spec.js
│ ├── analytics
│ │ ├── CampaignReportsTable.js
│ │ └── CampaignReportsTable.spec.js
│ ├── campaigns
│ │ ├── CreateCampaignForm.js
│ │ ├── CreateCampaignForm.spec.js
│ │ ├── ManageCampaigns.js
│ │ ├── ManageCampaigns.spec.js
│ │ ├── ManageCampaignsGraph.js
│ │ ├── ManageCampaignsGraph.spec.js
│ │ ├── ManageCampaignsTable.js
│ │ ├── ManageCampaignsTable.spec.js
│ │ ├── PreviewCampaignForm.js
│ │ └── PreviewCampaignForm.spec.js
│ ├── common
│ │ ├── DisabledLink.js
│ │ ├── DisabledLink.spec.js
│ │ ├── FormRenderWrappers.js
│ │ ├── FormRenderWrappers.spec.js
│ │ ├── SidebarLink.js
│ │ ├── SidebarLink.spec.js
│ │ ├── SidebarTreeview.js
│ │ ├── SidebarTreeview.spec.js
│ │ ├── TextEditorPlain.js
│ │ ├── TextEditorPlain.spec.js
│ │ ├── TextEditorRich.js
│ │ └── TextEditorRich.spec.js
│ ├── dashboard
│ │ ├── UserInfo.js
│ │ └── UserInfo.spec.js
│ ├── lists
│ │ ├── ErrorsList.js
│ │ ├── ErrorsList.spec.js
│ │ ├── ListSignupFormCreator.js
│ │ ├── ListSignupFormCreator.spec.js
│ │ ├── ManageLists.js
│ │ ├── ManageLists.spec.js
│ │ ├── ManageListsTable.js
│ │ ├── ManageListsTable.spec.js
│ │ ├── ManageSubscribersTable.js
│ │ ├── ManageSubscribersTable.spec.js
│ │ ├── SubscribersTable.js
│ │ └── SubscribersTable.spec.js
│ ├── permissions
│ │ ├── GrantPermissionForm.js
│ │ ├── GrantPermissionForm.spec.js
│ │ ├── ManageActivePermissionsTable.js
│ │ ├── ManageActivePermissionsTable.spec.js
│ │ ├── ManageGrantOfferedPermissionsTable.js
│ │ ├── ManageGrantOfferedPermissionsTable.spec.js
│ │ ├── ManageGrantedPermissionsTable.js
│ │ ├── ManageGrantedPermissionsTable.spec.js
│ │ ├── ManageReceivedPermissionOffersTable.js
│ │ └── ManageReceivedPermissionOffersTable.spec.js
│ └── templates
│ │ ├── CreateTemplateForm.js
│ │ ├── CreateTemplateForm.spec.js
│ │ ├── ManageTemplatesTable.js
│ │ ├── ManageTemplatesTable.spec.js
│ │ ├── PreviewTemplateForm.js
│ │ └── PreviewTemplateForm.spec.js
├── constants
│ ├── actionTypes.js
│ └── endpoints.js
├── containers
│ ├── AddEmail.js
│ ├── AddEmail.spec.js
│ ├── App.js
│ ├── App.spec.js
│ ├── Dashboard.js
│ ├── Dashboard.spec.js
│ ├── Notifications.js
│ ├── Notifications.spec.js
│ ├── Settings.js
│ ├── Settings.spec.js
│ ├── accountsManagement
│ │ ├── CreateAccount.js
│ │ └── DeleteAccount.js
│ ├── analytics
│ │ ├── CampaignReports.js
│ │ └── CampaignReports.spec.js
│ ├── campaigns
│ │ ├── CampaignView.js
│ │ ├── CampaignView.spec.js
│ │ ├── CreateCampaign.js
│ │ ├── CreateCampaign.spec.js
│ │ ├── ManageCampaignsBox.js
│ │ └── ManageCampaignsBox.spec.js
│ ├── common
│ │ ├── TextEditor.js
│ │ └── TextEditor.spec.js
│ ├── lists
│ │ ├── CreateList.js
│ │ ├── CreateList.spec.js
│ │ ├── ImportCSV.js
│ │ ├── ImportCSV.spec.js
│ │ ├── ManageListSubscribers.js
│ │ ├── ManageListSubscribers.spec.js
│ │ ├── ManageListsBox.js
│ │ └── ManageListsBox.spec.js
│ ├── permissions
│ │ ├── GrantPermissions.js
│ │ ├── GrantPermissions.spec.js
│ │ ├── OfferedPermissions.js
│ │ ├── OfferedPermissions.spec.js
│ │ ├── ReceivedPermissions.js
│ │ └── ReceivedPermissions.spec.js
│ └── templates
│ │ ├── CreateTemplate.js
│ │ ├── CreateTemplate.spec.js
│ │ ├── ManageTemplates.js
│ │ ├── ManageTemplates.spec.js
│ │ ├── TemplateView.js
│ │ └── TemplateView.spec.js
├── favicon.ico
├── index.ejs
├── index.js
├── reducers
│ ├── accountsManagementReducer.js
│ ├── appReducer.js
│ ├── appReducer.spec.js
│ ├── campaignReducer.js
│ ├── campaignReducer.spec.js
│ ├── index.js
│ ├── initialState.js
│ ├── listReducer.js
│ ├── listReducer.spec.js
│ ├── notificationsReducer.js
│ ├── notificationsReducer.spec.js
│ ├── permissionReducer.js
│ ├── permissionReducer.spec.js
│ ├── settingsReducer.js
│ └── settingsReducer.spec.js
├── routes.js
├── store
│ ├── configureStore.dev.js
│ ├── configureStore.js
│ └── configureStore.prod.js
├── styles
│ ├── header.scss
│ ├── index.scss
│ └── tables.scss
├── utils
│ ├── adminLte
│ │ ├── app.js
│ │ ├── assets
│ │ │ └── Lato-Regular.ttf
│ │ └── mail-for-good-theme.css
│ ├── deepFillArray.js
│ └── subscriberListParsers
│ │ ├── parseSubscriberList.js
│ │ └── parseSubscriberList.spec.js
└── webpack-public-path.js
├── config
└── paths.js
├── docker-compose.yml
├── docs
├── 1.png
├── 10.png
├── 11.png
├── 12.png
├── 13.png
├── 14.png
├── 15.png
├── 2.png
├── 3.png
├── 4.png
├── 5.png
├── 6.png
├── 7.png
├── 8.png
├── 9.png
├── aws-deployment-guide
│ └── index.html
├── aws_deploy.md
├── categories
│ └── index.xml
├── create_campaign_1.png
├── create_campaign_2.png
├── docs-hugo-templates
│ ├── archetypes
│ │ └── default.md
│ ├── config.toml
│ ├── content
│ │ ├── aws-deployment-guide.md
│ │ ├── getting-started
│ │ │ └── index.md
│ │ ├── google-api-guide.md
│ │ ├── how-to-contribute
│ │ │ └── index.md
│ │ ├── index.md
│ │ ├── license
│ │ │ └── index.md
│ │ ├── local-deployment-guide.md
│ │ ├── public
│ │ │ ├── 1.png
│ │ │ ├── 10.png
│ │ │ ├── 11.png
│ │ │ ├── 12.png
│ │ │ ├── 13.png
│ │ │ ├── 14.png
│ │ │ ├── 15.png
│ │ │ ├── 2.png
│ │ │ ├── 3.png
│ │ │ ├── 4.png
│ │ │ ├── 5.png
│ │ │ ├── 6.png
│ │ │ ├── 7.png
│ │ │ ├── 8.png
│ │ │ ├── 9.png
│ │ │ ├── aws-deployment-guide
│ │ │ │ └── index.html
│ │ │ ├── categories
│ │ │ │ └── index.xml
│ │ │ ├── create_campaign_1.png
│ │ │ ├── create_campaign_2.png
│ │ │ ├── elastic_allocate_new.png
│ │ │ ├── elastic_associate.png
│ │ │ ├── elastic_associate_conf.png
│ │ │ ├── fonts
│ │ │ │ ├── icon.eot
│ │ │ │ ├── icon.svg
│ │ │ │ ├── icon.ttf
│ │ │ │ └── icon.woff
│ │ │ ├── getting-started
│ │ │ │ ├── index.html
│ │ │ │ └── index.xml
│ │ │ ├── google-api-guide
│ │ │ │ └── index.html
│ │ │ ├── google_origins.png
│ │ │ ├── header_logo.jpeg
│ │ │ ├── hero.png
│ │ │ ├── how-to-contribute
│ │ │ │ ├── index.html
│ │ │ │ └── index.xml
│ │ │ ├── images
│ │ │ │ ├── favicon.ico
│ │ │ │ ├── logo.png
│ │ │ │ └── screen.png
│ │ │ ├── index.html
│ │ │ ├── index.xml
│ │ │ ├── javascripts
│ │ │ │ ├── application.js
│ │ │ │ ├── modal.js
│ │ │ │ ├── modernizr.js
│ │ │ │ └── test.js
│ │ │ ├── license
│ │ │ │ ├── index.html
│ │ │ │ └── index.xml
│ │ │ ├── local-deployment-guide
│ │ │ │ └── index.html
│ │ │ ├── logo.png
│ │ │ ├── modal.css
│ │ │ ├── modal.js
│ │ │ ├── navbar_ec2_elasticIP.png
│ │ │ ├── settings.png
│ │ │ ├── sitemap.xml
│ │ │ ├── static
│ │ │ │ ├── javascripts
│ │ │ │ │ └── modal.js
│ │ │ │ └── stylesheets
│ │ │ │ │ └── modal.css
│ │ │ ├── stylesheets
│ │ │ │ ├── application.css
│ │ │ │ ├── highlight
│ │ │ │ │ ├── github_highlightjs.css
│ │ │ │ │ └── highlight.css
│ │ │ │ ├── index.css
│ │ │ │ ├── misc.css
│ │ │ │ ├── modal.css
│ │ │ │ ├── palettes.css
│ │ │ │ └── temporary.css
│ │ │ └── tags
│ │ │ │ └── index.xml
│ │ └── static
│ │ │ ├── javascripts
│ │ │ └── modal.js
│ │ │ └── stylesheets
│ │ │ └── modal.css
│ ├── layouts
│ │ └── partials
│ │ │ └── footer.html
│ ├── static
│ │ ├── 1.png
│ │ ├── 10.png
│ │ ├── 11.png
│ │ ├── 12.png
│ │ ├── 13.png
│ │ ├── 14.png
│ │ ├── 15.png
│ │ ├── 2.png
│ │ ├── 3.png
│ │ ├── 4.png
│ │ ├── 5.png
│ │ ├── 6.png
│ │ ├── 7.png
│ │ ├── 8.png
│ │ ├── 9.png
│ │ ├── create_campaign_1.png
│ │ ├── create_campaign_2.png
│ │ ├── elastic_allocate_new.png
│ │ ├── elastic_associate.png
│ │ ├── elastic_associate_conf.png
│ │ ├── google_origins.png
│ │ ├── header_logo.jpeg
│ │ ├── hero.png
│ │ ├── javascripts
│ │ │ └── modal.js
│ │ ├── logo.png
│ │ ├── navbar_ec2_elasticIP.png
│ │ ├── settings.png
│ │ └── stylesheets
│ │ │ ├── highlight
│ │ │ └── highlight.css
│ │ │ ├── misc.css
│ │ │ ├── modal.css
│ │ │ └── palettes.css
│ └── themes
│ │ └── hugo-material-docs
│ │ ├── CHANGELOG.md
│ │ ├── LICENSE.md
│ │ ├── README.md
│ │ ├── archetypes
│ │ └── default.md
│ │ ├── exampleSite
│ │ ├── config.toml
│ │ ├── content
│ │ │ ├── adding-content
│ │ │ │ └── index.md
│ │ │ ├── getting-started
│ │ │ │ └── index.md
│ │ │ ├── index.md
│ │ │ ├── license
│ │ │ │ └── index.md
│ │ │ └── roadmap
│ │ │ │ └── index.md
│ │ └── static
│ │ │ └── .gitkeep
│ │ ├── images
│ │ ├── screenshot.png
│ │ └── tn.png
│ │ ├── layouts
│ │ ├── 404.html
│ │ ├── _default
│ │ │ ├── __list.html
│ │ │ └── single.html
│ │ ├── index.html
│ │ ├── partials
│ │ │ ├── drawer.html
│ │ │ ├── footer.html
│ │ │ ├── footer_js.html
│ │ │ ├── head.html
│ │ │ ├── header.html
│ │ │ ├── nav.html
│ │ │ └── nav_link.html
│ │ └── shortcodes
│ │ │ ├── note.html
│ │ │ └── warning.html
│ │ ├── static
│ │ ├── fonts
│ │ │ ├── icon.eot
│ │ │ ├── icon.svg
│ │ │ ├── icon.ttf
│ │ │ └── icon.woff
│ │ ├── images
│ │ │ ├── favicon.ico
│ │ │ ├── logo.png
│ │ │ └── screen.png
│ │ ├── javascripts
│ │ │ ├── application.js
│ │ │ └── modernizr.js
│ │ └── stylesheets
│ │ │ ├── application.css
│ │ │ ├── highlight
│ │ │ └── highlight.css
│ │ │ ├── palettes.css
│ │ │ └── temporary.css
│ │ └── theme.toml
├── elastic_allocate_new.png
├── elastic_associate.png
├── elastic_associate_conf.png
├── fonts
│ ├── icon.eot
│ ├── icon.svg
│ ├── icon.ttf
│ └── icon.woff
├── getting-started
│ ├── index.html
│ └── index.xml
├── google-api-guide
│ └── index.html
├── google_origins.png
├── header_logo.jpeg
├── hero.png
├── how-to-contribute
│ ├── index.html
│ └── index.xml
├── images
│ ├── favicon.ico
│ ├── logo.png
│ └── screen.png
├── index.html
├── index.xml
├── javascripts
│ ├── application.js
│ ├── modal.js
│ └── modernizr.js
├── license
│ ├── index.html
│ └── index.xml
├── local-deployment-guide
│ └── index.html
├── local_deploy.md
├── logo.png
├── navbar_ec2_elasticIP.png
├── planned_features.md
├── public
│ ├── 1.png
│ ├── 10.png
│ ├── 11.png
│ ├── 12.png
│ ├── 13.png
│ ├── 14.png
│ ├── 15.png
│ ├── 2.png
│ ├── 3.png
│ ├── 4.png
│ ├── 5.png
│ ├── 6.png
│ ├── 7.png
│ ├── 8.png
│ ├── 9.png
│ ├── aws-deployment-guide
│ │ └── index.html
│ ├── categories
│ │ └── index.xml
│ ├── create_campaign_1.png
│ ├── create_campaign_2.png
│ ├── elastic_allocate_new.png
│ ├── elastic_associate.png
│ ├── elastic_associate_conf.png
│ ├── fonts
│ │ ├── icon.eot
│ │ ├── icon.svg
│ │ ├── icon.ttf
│ │ └── icon.woff
│ ├── getting-started
│ │ ├── index.html
│ │ └── index.xml
│ ├── google-api-guide
│ │ └── index.html
│ ├── google_origins.png
│ ├── header_logo.jpeg
│ ├── hero.png
│ ├── how-to-contribute
│ │ ├── index.html
│ │ └── index.xml
│ ├── images
│ │ ├── favicon.ico
│ │ ├── logo.png
│ │ └── screen.png
│ ├── index.html
│ ├── index.xml
│ ├── javascripts
│ │ ├── application.js
│ │ ├── modal.js
│ │ ├── modernizr.js
│ │ └── test.js
│ ├── license
│ │ ├── index.html
│ │ └── index.xml
│ ├── local-deployment-guide
│ │ └── index.html
│ ├── logo.png
│ ├── modal.css
│ ├── modal.js
│ ├── navbar_ec2_elasticIP.png
│ ├── settings.png
│ ├── sitemap.xml
│ ├── static
│ │ ├── javascripts
│ │ │ └── modal.js
│ │ └── stylesheets
│ │ │ └── modal.css
│ ├── stylesheets
│ │ ├── application.css
│ │ ├── highlight
│ │ │ ├── github_highlightjs.css
│ │ │ └── highlight.css
│ │ ├── index.css
│ │ ├── misc.css
│ │ ├── modal.css
│ │ ├── palettes.css
│ │ └── temporary.css
│ └── tags
│ │ └── index.xml
├── resources
│ ├── deploy_images
│ │ ├── 1.png
│ │ ├── 10.png
│ │ ├── 11.png
│ │ ├── 12.png
│ │ ├── 13.png
│ │ ├── 14.png
│ │ ├── 15.png
│ │ ├── 2.png
│ │ ├── 3.png
│ │ ├── 4.png
│ │ ├── 5.png
│ │ ├── 6.png
│ │ ├── 7.png
│ │ ├── 8.png
│ │ ├── 9.png
│ │ ├── elastic_allocate_new.png
│ │ ├── elastic_associate.png
│ │ ├── elastic_associate_conf.png
│ │ ├── google_origins.png
│ │ └── navbar_ec2_elasticIP.png
│ └── hero.png
├── setting_up.md
├── settings.png
├── sitemap.xml
├── static
│ ├── javascripts
│ │ └── modal.js
│ └── stylesheets
│ │ └── modal.css
├── stylesheets
│ ├── application.css
│ ├── highlight
│ │ └── highlight.css
│ ├── misc.css
│ ├── modal.css
│ ├── palettes.css
│ └── temporary.css
└── tags
│ └── index.xml
├── package-lock.json
├── package.json
├── public
├── assets
│ ├── btn_google_signin_dark_normal_web@2x.png
│ ├── btn_google_signin_light_disabled_web@2x.png
│ ├── btn_google_signin_light_focus_web@2x.png
│ ├── btn_google_signin_light_normal_web@2x.png
│ └── btn_google_signin_light_pressed_web@2x.png
├── index.pug
└── main.css
├── server
├── config
│ ├── passport
│ │ ├── google.js
│ │ └── local.js
│ ├── secrets.js
│ ├── sequelize_config.js
│ └── server
│ │ ├── index.js
│ │ ├── io.js
│ │ ├── passport.js
│ │ ├── redis.js
│ │ ├── restore-db-state.js
│ │ ├── sequelize.js
│ │ ├── session.js
│ │ └── webpack-dev-middleware.js
├── controllers
│ ├── accountsManagement
│ │ ├── create-user.js
│ │ └── delete-user.js
│ ├── analytics
│ │ ├── clickthrough.js
│ │ ├── get-clickthroughs.js
│ │ ├── open.js
│ │ └── refresh.js
│ ├── campaign
│ │ ├── create-campaign.js
│ │ ├── create-campaign.spec.js
│ │ ├── delete-campaigns.js
│ │ ├── email
│ │ │ ├── amazon-ses
│ │ │ │ ├── README.md
│ │ │ │ ├── config
│ │ │ │ │ └── configSes.js
│ │ │ │ ├── controllers
│ │ │ │ │ ├── finishCampaignSend.js
│ │ │ │ │ ├── getAmazonEmailArray.js
│ │ │ │ │ ├── getArrayOfEmailIds.js
│ │ │ │ │ └── updateCampaignStatus.js
│ │ │ │ ├── index.js
│ │ │ │ ├── lib
│ │ │ │ │ ├── amazon.js
│ │ │ │ │ ├── analytics.js
│ │ │ │ │ ├── analytics.spec.js
│ │ │ │ │ ├── mail-merge.js
│ │ │ │ │ ├── mail-merge.spec.js
│ │ │ │ │ ├── nest-array.js
│ │ │ │ │ └── nest-array.spec.js
│ │ │ │ ├── notifications
│ │ │ │ │ ├── sendFinalNotification.js
│ │ │ │ │ └── sendUpdateEmailsSentNotification.js
│ │ │ │ ├── queue
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── sendEmail.js
│ │ │ │ ├── send-test.js
│ │ │ │ └── utils
│ │ │ │ │ └── sendCampaignSuccessEmail.js
│ │ │ └── index.js
│ │ ├── export-sent-unsent-csv.js
│ │ ├── get-campaigns.js
│ │ ├── send-campaign.js
│ │ ├── send-campaign.spec.js
│ │ ├── stop-campaign-sending.js
│ │ └── stop-campaign-sending.spec.js
│ ├── list
│ │ ├── add-subscribers.js
│ │ ├── delete-lists.js
│ │ ├── delete-subscribers.js
│ │ ├── export-list-subscribers-csv.js
│ │ ├── get-list-subscribers.js
│ │ ├── get-lists.js
│ │ ├── import-csv.js
│ │ ├── subscribe.js
│ │ └── update-list.js
│ ├── permissions
│ │ ├── accept-permission-offer.js
│ │ ├── acl-lib
│ │ │ ├── acl-campaign-permissions.js
│ │ │ ├── acl-list-permissions.js
│ │ │ └── acl-template-permissions.js
│ │ ├── delete-active-permissions.js
│ │ ├── delete-grant-offered-permissions.js
│ │ ├── delete-granted-permissions.js
│ │ ├── get-active-permissions.js
│ │ ├── get-grant-offered-permissions.js
│ │ ├── get-granted-permissions.js
│ │ ├── get-received-permission-offers.js
│ │ ├── grant-permission.js
│ │ └── reject-permission-offers.js
│ ├── settings
│ │ ├── changesettings.js
│ │ ├── configure-aws
│ │ │ └── configure-aws.js
│ │ └── get-settings.js
│ ├── subscriber
│ │ └── unsubscribe.js
│ ├── template
│ │ ├── create-template.js
│ │ ├── delete-templates.js
│ │ └── get-templates.js
│ └── websockets
│ │ ├── get-profile.js
│ │ ├── send-single-notification.js
│ │ └── send-update-notification.js
├── feedback-consumer
│ ├── consumer.js
│ └── feedback-consumer.js
├── index.js
├── migrations
│ └── 20180521070624-pass-to-classical-auth-flow.js
├── models
│ ├── acl.js
│ ├── analysis.js
│ ├── analysisemail.js
│ ├── campaign.js
│ ├── campaignanalytics.js
│ ├── campaignanalyticslink.js
│ ├── campaignanalyticsopen.js
│ ├── campaignsubscriber.js
│ ├── index.js
│ ├── list.js
│ ├── listsubscriber.js
│ ├── offerPermission.js
│ ├── setting.js
│ ├── subscriber.js
│ ├── template.js
│ └── user.js
├── routes
│ ├── accountsManagement.js
│ ├── auth.js
│ ├── campaigns.js
│ ├── index.js
│ ├── lists.js
│ ├── middleware
│ │ ├── acl.js
│ │ └── auth.js
│ ├── permissions.js
│ └── templates.js
└── tests
│ ├── controllers
│ ├── campaigns
│ │ └── export-list-subscribers-csv.func.js
│ └── lists
│ │ ├── import-csv.func.js
│ │ ├── stop-campaign-sending.func.js
│ │ └── test-csv-files
│ │ └── 10emailswithtoomanycommas
│ └── mocha.opts
├── tools
├── build.js
├── chalkConfig.js
├── setup
│ └── initial_setup.py
├── testClientSetup.js
└── testSetup.js
├── utility
└── generateEmailCsv.py
├── webpack.config.build.js
└── webpack.config.dev.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | "transform-decorators-legacy"
4 | ],
5 | "presets": [
6 | "latest",
7 | "react",
8 | "stage-1"
9 | ],
10 | "env": {
11 | "development": {
12 | "presets": [
13 | "react-hmre"
14 | ]
15 | },
16 | "production": {
17 | "plugins": [
18 | "transform-react-constant-elements",
19 | "transform-react-remove-prop-types"
20 | ]
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/.sequelizerc:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | config: './server/config/sequelize_config.js',
3 | migrationsPath: './server/migrations',
4 | modelsPath: './server/models',
5 | seedersPath: './server/seed'
6 | };
7 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | All Open Source for Good projects follow Free Code Camp's [contributor's guide](https://github.com/freeCodeCamp/freeCodeCamp/blob/master/CONTRIBUTING.md).
2 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:8.2.1-onbuild
2 |
3 | # Copying package.json and running
4 | # npm install are automatically handled
5 |
6 | # Add source files
7 | COPY . .
8 |
9 | # And then compile the frontend
10 | RUN npm run build
11 |
12 | CMD ["npm", "start"]
13 | EXPOSE 8080
14 |
--------------------------------------------------------------------------------
/client/actions/appActions.spec.js:
--------------------------------------------------------------------------------
1 | // Test actions not indirectly tested in reducers
2 | import { expect } from 'chai';
3 |
4 | import {
5 | emitProfileRequest,
6 | localNotification
7 | } from './appActions';
8 |
9 | describe('(Actions) app', () => {
10 |
11 | it('should exist', () => {
12 | expect(emitProfileRequest).to.be.a('function');
13 | expect(localNotification).to.be.a('function');
14 | });
15 |
16 | });
--------------------------------------------------------------------------------
/client/actions/campaignActions.spec.js:
--------------------------------------------------------------------------------
1 | // Test actions not indirectly tested in reducers
2 | import { expect } from 'chai';
3 |
4 | import {
5 | requestStopSending,
6 | completeStopSending,
7 | stopSending,
8 | getCampaigns,
9 | postCreateTemplate,
10 | postCreateCampaign,
11 | deleteCampaigns,
12 | postSendCampaign,
13 | postTestEmail,
14 | getTemplates,
15 | deleteTemplates
16 | } from './campaignActions';
17 |
18 | describe('(Actions) campaign', () => {
19 |
20 | it('should exist', () => {
21 | expect(requestStopSending).to.be.a('function');
22 | expect(completeStopSending).to.be.a('function');
23 | expect(stopSending).to.be.a('function');
24 | expect(getCampaigns).to.be.a('function');
25 | expect(postCreateTemplate).to.be.a('function');
26 | expect(postCreateCampaign).to.be.a('function');
27 | expect(deleteCampaigns).to.be.a('function');
28 | expect(postSendCampaign).to.be.a('function');
29 | expect(postTestEmail).to.be.a('function');
30 | expect(getTemplates).to.be.a('function');
31 | expect(deleteTemplates).to.be.a('function');
32 | });
33 |
34 | });
--------------------------------------------------------------------------------
/client/actions/listActions.spec.js:
--------------------------------------------------------------------------------
1 | // Test actions not indirectly tested in reducers
2 | import { expect } from 'chai';
3 |
4 | import {
5 | getListSubscribers,
6 | getLists,
7 | submitCSV,
8 | deleteListSubscribers,
9 | deleteLists
10 | } from './listActions';
11 |
12 | describe('(Actions) list', () => {
13 |
14 | it('should exist', () => {
15 | expect(getListSubscribers).to.be.a('function');
16 | expect(getLists).to.be.a('function');
17 | expect(submitCSV).to.be.a('function');
18 | expect(deleteListSubscribers).to.be.a('function');
19 | expect(deleteLists).to.be.a('function');
20 | });
21 |
22 | });
--------------------------------------------------------------------------------
/client/actions/notificationActions.js:
--------------------------------------------------------------------------------
1 | import { RECEIVE_NOTIFICATION, CONSUME_NOTIFICATION } from '../constants/actionTypes';
2 |
3 | export function notify(notification) {
4 | return {
5 | type: RECEIVE_NOTIFICATION,
6 | notification: {
7 | message: notification.message,
8 | dismissAfter: 20000,
9 | isActive: true,
10 | activeBarStyle: {
11 | background: notification.colour === 'green' ? 'green' : 'crimson',
12 | left: ''
13 | }
14 | }};
15 | }
16 |
17 | export function consume() {
18 | return { type: CONSUME_NOTIFICATION };
19 | }
20 |
--------------------------------------------------------------------------------
/client/actions/settingsActions.spec.js:
--------------------------------------------------------------------------------
1 | // Test actions not indirectly tested in reducers
2 | import { expect } from 'chai';
3 |
4 | import {
5 | getBooleanForAssignedSettings,
6 | changeSettings
7 | } from './settingsActions';
8 |
9 | describe('(Actions) settings', () => {
10 |
11 | it('should exist', () => {
12 | expect(getBooleanForAssignedSettings).to.be.a('function');
13 | expect(changeSettings).to.be.a('function');
14 | });
15 |
16 | });
--------------------------------------------------------------------------------
/client/components/404/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const NotFound = () => {
4 | return (
5 |
6 |
7 | Page not found
8 | Please check the URL
9 |
10 |
11 |
12 | );
13 | };
14 |
15 | export default NotFound;
16 |
--------------------------------------------------------------------------------
/client/components/404/index.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import NotFound from '../404';
6 |
7 | const wrapper = shallow();
8 |
9 | describe('(Component) NotFound', () => {
10 | it('renders without exploding', () => {
11 | expect(wrapper).to.have.lengthOf(1);
12 | });
13 | });
--------------------------------------------------------------------------------
/client/components/admin-lte/Footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Footer = (props) => { // eslint-disable-line no-unused-vars
4 | return (
5 |
11 | );
12 | };
13 |
14 | export default Footer;
15 |
--------------------------------------------------------------------------------
/client/components/admin-lte/Footer.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import Footer from './Footer';
6 |
7 | const wrapper = shallow();
8 |
9 | describe('(Component) Footer', () => {
10 | it('renders without exploding', () => {
11 | expect(wrapper).to.have.lengthOf(1);
12 | });
13 | });
--------------------------------------------------------------------------------
/client/components/admin-lte/Header.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import Header from './Header';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | user: {},
9 | ws_notification: [],
10 | consumeNotification: () => {},
11 | ...overrides
12 | });
13 |
14 | const wrapper = shallow();
15 |
16 | describe('(Component) Header', () => {
17 | it('renders without exploding', () => {
18 | expect(wrapper).to.have.lengthOf(1);
19 | });
20 | });
--------------------------------------------------------------------------------
/client/components/admin-lte/RightSidebar.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import RightSidebar from './RightSidebar';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | touch: () => {},
9 | valid: true,
10 | pristine: true,
11 | submitting: true,
12 | changeAccount: () => {},
13 | changeAccountToSelf: () => {},
14 | isGettingActivePermissions: true,
15 | activePermissionsEmails: [],
16 | activeAccount: {},
17 | ...overrides
18 | });
19 |
20 | const wrapper = shallow();
21 |
22 | describe('(Component) RightSidebar', () => {
23 | it('renders without exploding', () => {
24 | expect(wrapper).to.have.lengthOf(1);
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/client/components/admin-lte/Sidebar.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import Sidebar from './Sidebar';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | user: {},
9 | activeAccount: {}
10 | });
11 |
12 | const wrapper = shallow();
13 |
14 | describe('(Component) Sidebar', () => {
15 | it('renders without exploding', () => {
16 | expect(wrapper).to.have.lengthOf(1);
17 | });
18 | });
--------------------------------------------------------------------------------
/client/components/admin-lte/WS-Notification.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import ReactTooltip from 'react-tooltip';
4 | import { Link } from 'react-router';
5 |
6 | const WSNotification = props => { // eslint-disable-line no-unused-vars
7 | const { message, icon, iconColour, consumeNotification, index, url } = props;
8 | return (
9 | consumeNotification(index)}>
10 |
11 |
12 | {message}
13 |
14 |
15 |
16 |
17 |
18 | );
19 | };
20 |
21 | WSNotification.propTypes = {
22 | message: PropTypes.string.isRequired,
23 | icon: PropTypes.string,
24 | iconColour: PropTypes.string,
25 | consumeNotification: PropTypes.func.isRequired,
26 | index: PropTypes.number.isRequired,
27 | url: PropTypes.string
28 | };
29 |
30 | export default WSNotification;
31 |
--------------------------------------------------------------------------------
/client/components/admin-lte/WS-Notification.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import WSNotification from './WS-Notification';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | message: '',
9 | icon: '',
10 | iconColour: '',
11 | consumeNotification: () => {},
12 | index: 0,
13 | ...overrides
14 | });
15 |
16 | const wrapper = shallow();
17 |
18 | describe('(Component) WSNotification', () => {
19 | it('renders without exploding', () => {
20 | expect(wrapper).to.have.lengthOf(1);
21 | });
22 | });
--------------------------------------------------------------------------------
/client/components/analytics/CampaignReportsTable.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import CampaignReportsTable from './CampaignReportsTable';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | data: [],
9 | ...overrides
10 | });
11 |
12 | const wrapper = shallow();
13 |
14 | describe('(Component) CampaignReportsTable', () => {
15 | it('renders without exploding', () => {
16 | expect(wrapper).to.have.lengthOf(1);
17 | });
18 | });
--------------------------------------------------------------------------------
/client/components/campaigns/CreateCampaignForm.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import CreateCampaignForm from './CreateCampaignForm';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | touch: () => {},
9 | valid: true,
10 | pristine: true,
11 | submitting: true,
12 | nextPage: () => {},
13 | reset: () => {},
14 | lists: [],
15 | templates: [],
16 | applyTemplate: () => {},
17 | textEditorType: '',
18 | passResetToState: () => {},
19 | initialValues: {},
20 | ...overrides
21 | });
22 |
23 | const wrapper = shallow();
24 |
25 | describe('(Component) CreateCampaignForm', () => {
26 | it('renders without exploding', () => {
27 | expect(wrapper).to.have.lengthOf(1);
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/client/components/campaigns/ManageCampaigns.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ManageCampaignsBox from '../../containers/campaigns/ManageCampaignsBox';
3 |
4 | const ManageCampaigns = () => {
5 | return (
6 |
7 |
8 |
Manage campaigns
9 | Edit or delete your campaigns here
10 |
11 |
12 |
13 |
16 |
17 | );
18 | };
19 |
20 | export default ManageCampaigns;
21 |
--------------------------------------------------------------------------------
/client/components/campaigns/ManageCampaigns.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import ManageCampaigns from './ManageCampaigns';
6 |
7 | const wrapper = shallow();
8 |
9 | describe('(Component) ManageCampaigns', () => {
10 | it('renders without exploding', () => {
11 | expect(wrapper).to.have.lengthOf(1);
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/client/components/campaigns/ManageCampaignsGraph.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import ManageCampaignsGraph from './ManageCampaignsGraph';
6 |
7 | const mockProps = (overrides) => ({
8 | data: [],
9 | ...overrides
10 | });
11 |
12 | const wrapper = shallow();
13 |
14 | describe('(Component) ManageCampaignsGraph', () => {
15 | it('renders without exploding', () => {
16 | expect(wrapper).to.have.lengthOf(1);
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/client/components/campaigns/ManageCampaignsTable.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import ManageCampaignsTable from './ManageCampaignsTable';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | data: [],
9 | deleteRows: () => {},
10 | getCampaignView: () => {},
11 | ...overrides
12 | });
13 |
14 | const wrapper = shallow();
15 |
16 | describe('(Component) ManageCampaignsTable', () => {
17 | it('renders without exploding', () => {
18 | expect(wrapper).to.have.lengthOf(1);
19 | });
20 | });
--------------------------------------------------------------------------------
/client/components/campaigns/PreviewCampaignForm.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import PreviewCampaignForm from './PreviewCampaignForm';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | handleSubmit: () => {},
9 | lastPage: () => {},
10 | form: { values: {} },
11 | campaignView: {},
12 | ...overrides
13 | });
14 |
15 | const wrapper = shallow();
16 |
17 | describe('(Component) PreviewCampaignForm', () => {
18 | it('renders without exploding', () => {
19 | expect(wrapper).to.have.lengthOf(1);
20 | });
21 | });
--------------------------------------------------------------------------------
/client/components/common/DisabledLink.js:
--------------------------------------------------------------------------------
1 | /*eslint-disable react/prop-types*/
2 | import React, { Component, PropTypes } from 'react';
3 |
4 | class DisabledLink extends Component {
5 |
6 | render() {
7 | return (
8 |
9 |
10 | {this.props.children}
11 |
12 |
13 | );
14 | }
15 | }
16 |
17 | DisabledLink.contextTypes = {
18 | router: PropTypes.object.isRequired
19 | };
20 |
21 | DisabledLink.propTypes = {
22 | icon: PropTypes.string,
23 | children: PropTypes.string.isRequired
24 | };
25 |
26 | export default DisabledLink;
27 |
--------------------------------------------------------------------------------
/client/components/common/DisabledLink.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import DisabledLink from './DisabledLink';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | icon: '',
9 | children: '',
10 | ...overrides
11 | });
12 |
13 | const mockContext = {
14 | router: { isActive: (a, b) => true } // eslint-disable-line no-unused-vars
15 | };
16 |
17 | const wrapper = shallow(, { context: mockContext });
18 |
19 | describe('(Component) DisabledLink', () => {
20 | it('renders without exploding', () => {
21 | expect(wrapper).to.have.lengthOf(1);
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/client/components/common/SidebarLink.js:
--------------------------------------------------------------------------------
1 | /*eslint-disable react/prop-types*/
2 | import React, {Component, PropTypes} from 'react';
3 | import {Link} from 'react-router';
4 |
5 | class SidebarLink extends Component {
6 |
7 | render() {
8 | return (
9 |
12 |
13 | {this.props.children}
14 |
15 |
16 | );
17 | }
18 | }
19 |
20 | SidebarLink.contextTypes = {
21 | router: PropTypes.object.isRequired
22 | };
23 |
24 | SidebarLink.propTypes = {
25 | to: PropTypes.string.isRequired,
26 | icon: PropTypes.string,
27 | children: PropTypes.string.isRequired
28 | };
29 |
30 | export default SidebarLink;
31 |
--------------------------------------------------------------------------------
/client/components/common/SidebarLink.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import SidebarLink from './SidebarLink';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | to: '',
9 | icon: '',
10 | children: '',
11 | ...overrides
12 | });
13 |
14 | const mockContext = {
15 | router: { isActive: (a, b) => true } // eslint-disable-line no-unused-vars
16 | };
17 |
18 | const wrapper = shallow(, { context: mockContext });
19 |
20 | describe('(Component) SidebarLink', () => {
21 | it('renders without exploding', () => {
22 | expect(wrapper).to.have.lengthOf(1);
23 | });
24 | });
--------------------------------------------------------------------------------
/client/components/common/SidebarTreeview.js:
--------------------------------------------------------------------------------
1 | /*eslint-disable react/prop-types*/
2 | import React, { Component, PropTypes } from 'react';
3 |
4 | class SidebarTreeview extends Component {
5 |
6 | render() {
7 | return (
8 |
9 |
10 | {this.props.name}
11 |
12 |
13 | {this.props.children}
14 |
15 |
16 | );
17 | }
18 | }
19 |
20 | SidebarTreeview.contextTypes = {
21 | router: PropTypes.object.isRequired
22 | };
23 |
24 | SidebarTreeview.propTypes = {
25 | name: PropTypes.string.isRequired,
26 | icon: PropTypes.string.isRequired
27 | };
28 |
29 | export default SidebarTreeview;
30 |
--------------------------------------------------------------------------------
/client/components/common/SidebarTreeview.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import SidebarTreeview from './SidebarTreeview';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | name: '',
9 | icon: '',
10 | ...overrides
11 | });
12 |
13 | const mockContext = {
14 | router: { isActive: (a, b) => true } // eslint-disable-line no-unused-vars
15 | };
16 |
17 | const wrapper = shallow(, { context: mockContext });
18 |
19 | describe('(Component) SidebarTreeview', () => {
20 | it('renders without exploding', () => {
21 | expect(wrapper).to.have.lengthOf(1);
22 | });
23 | });
--------------------------------------------------------------------------------
/client/components/common/TextEditorPlain.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 |
3 | export default class TextEditorPlain extends Component {
4 | static propTypes = {
5 | value: PropTypes.string,
6 | onChange: PropTypes.func
7 | }
8 |
9 | render() {
10 | const { value, onChange } = this.props;
11 | return (
12 |
18 | );
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/client/components/common/TextEditorPlain.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import TextEditorPlain from './TextEditorPlain';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | value: '',
9 | onChange: () => {},
10 | ...overrides
11 | });
12 |
13 | const wrapper = shallow();
14 |
15 | describe('(Component) TextEditorPlain', () => {
16 | it('renders without exploding', () => {
17 | expect(wrapper).to.have.lengthOf(1);
18 | });
19 | });
--------------------------------------------------------------------------------
/client/components/common/TextEditorRich.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import TextEditorRich from './TextEditorRich';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | value: '',
9 | onChange: () => {},
10 | ...overrides
11 | });
12 |
13 | const wrapper = shallow();
14 |
15 | describe('(Component) TextEditorRich', () => {
16 | it('renders without exploding', () => {
17 | expect(wrapper).to.have.lengthOf(1);
18 | });
19 | });
--------------------------------------------------------------------------------
/client/components/dashboard/UserInfo.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 |
3 | const UserInfo = props => {
4 | const { user, totalSentCount } = props;
5 | return (
6 |
7 |
8 |
{`Welcome ${user.name}`}
9 |
10 |
11 |
12 |
{user.email}
13 |
Total emails sent: {totalSentCount}
14 |
15 |
16 | );
17 | };
18 |
19 | UserInfo.propTypes = {
20 | user: PropTypes.object.isRequired,
21 | totalSentCount: PropTypes.number
22 | };
23 |
24 | export default UserInfo;
25 |
--------------------------------------------------------------------------------
/client/components/dashboard/UserInfo.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import UserInfo from './UserInfo';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | user: {},
9 | totalSentCount: 0,
10 | ...overrides
11 | });
12 |
13 | const wrapper = shallow();
14 |
15 | describe('(Component) UserInfo', () => {
16 | it('renders without exploding', () => {
17 | expect(wrapper).to.have.lengthOf(1);
18 | });
19 | });
--------------------------------------------------------------------------------
/client/components/lists/ErrorsList.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | export default class ErrorsList extends Component {
4 | constructor(props) {
5 | super(props);
6 |
7 | this.state = {
8 | errors: this.props.errors || []
9 | };
10 | }
11 |
12 | componentWillReceiveProps(newProps) {
13 | this.setState({
14 | errors: newProps.errors || []
15 | });
16 | }
17 |
18 | render() {
19 | const numberOfErrors = this.state.errors.length;
20 |
21 | if (numberOfErrors === 0) {
22 | return ;
23 | }
24 |
25 | return (
26 |
27 |
28 |
29 |
{numberOfErrors} errors encountered while parsing file
30 |
31 | {this.state.errors.map((error, index) => {
32 | return (
33 |
34 | Row {error.row} {error.message}
35 |
36 | );
37 | })}
38 |
39 | );
40 | }
41 | }
42 |
43 | ErrorsList.propTypes = {
44 | errors: React.PropTypes.array
45 | };
46 |
--------------------------------------------------------------------------------
/client/components/lists/ErrorsList.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import ErrorsList from './ErrorsList';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | errors: [],
9 | ...overrides
10 | });
11 |
12 | const wrapper = shallow();
13 |
14 | describe('(Component) ErrorsList', () => {
15 | it('renders without exploding', () => {
16 | expect(wrapper).to.have.lengthOf(1);
17 | });
18 | });
--------------------------------------------------------------------------------
/client/components/lists/ListSignupFormCreator.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import { ListSignupFormCreatorComponent } from './ListSignupFormCreator';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | showModal: true,
9 | subscribeKey: '',
10 | notify: () => {},
11 | ...overrides
12 | });
13 |
14 | const wrapper = shallow();
15 |
16 | describe('(Component) ListSignupFormCreator', () => {
17 | it('renders without exploding', () => {
18 | expect(wrapper).to.have.lengthOf(1);
19 | });
20 | });
--------------------------------------------------------------------------------
/client/components/lists/ManageLists.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import ManageListsBox from '../../containers/lists/ManageListsBox';
4 |
5 | const ManageLists = () => {
6 | return (
7 |
8 |
9 |
Manage lists
10 | Edit, delete and segement your lists here
11 |
12 |
13 |
14 |
17 |
18 | );
19 | };
20 |
21 | export default ManageLists;
22 |
--------------------------------------------------------------------------------
/client/components/lists/ManageLists.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import ManageLists from './ManageLists';
6 |
7 | const wrapper = shallow();
8 |
9 | describe('(Component) ManageLists', () => {
10 | it('renders without exploding', () => {
11 | expect(wrapper).to.have.lengthOf(1);
12 | });
13 | });
--------------------------------------------------------------------------------
/client/components/lists/ManageListsTable.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import ManageListsTable from './ManageListsTable';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | data: [],
9 | deleteRows: () => {},
10 | ...overrides
11 | });
12 |
13 | const wrapper = shallow();
14 |
15 | describe('(Component) ManageListsTable', () => {
16 | it('renders without exploding', () => {
17 | expect(wrapper).to.have.lengthOf(1);
18 | });
19 | });
--------------------------------------------------------------------------------
/client/components/lists/ManageSubscribersTable.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import ManageSubscribersTable from './ManageSubscribersTable';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | data: [],
9 | deleteRows: () => {},
10 | onPageChange: () => {},
11 | additionalFields: [],
12 | listId: 0,
13 | total: 0,
14 | ...overrides
15 | });
16 |
17 | const wrapper = shallow();
18 |
19 | describe('(Component) ManageSubscribersTable', () => {
20 | it('renders without exploding', () => {
21 | expect(wrapper).to.have.lengthOf(1);
22 | });
23 | });
--------------------------------------------------------------------------------
/client/components/lists/SubscribersTable.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import SubscribersTable from './SubscribersTable';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | subscribers: [],
9 | fields: [],
10 | ...overrides
11 | });
12 |
13 | const wrapper = shallow();
14 |
15 | describe('(Component) SubscribersTable', () => {
16 | it('renders without exploding', () => {
17 | expect(wrapper).to.have.lengthOf(1);
18 | });
19 | });
--------------------------------------------------------------------------------
/client/components/permissions/GrantPermissionForm.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import GrantPermissionForm from './GrantPermissionForm';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | touch: () => {},
9 | valid: true,
10 | pristine: true,
11 | submitting: true,
12 | reset: () => {},
13 | handleSubmit: () => {},
14 | ...overrides
15 | });
16 |
17 | const wrapper = shallow();
18 |
19 | describe('(Component) GrantPermissionForm', () => {
20 | it('renders without exploding', () => {
21 | expect(wrapper).to.have.lengthOf(1);
22 | });
23 | });
--------------------------------------------------------------------------------
/client/components/permissions/ManageActivePermissionsTable.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import ManageActivePermissionsTable from './ManageActivePermissionsTable';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | data: [],
9 | deletePermissionRows: () => {},
10 | ...overrides
11 | });
12 |
13 | const wrapper = shallow();
14 |
15 | describe('(Component) ManageActivePermissionsTable', () => {
16 | it('renders without exploding', () => {
17 | expect(wrapper).to.have.lengthOf(1);
18 | });
19 | });
--------------------------------------------------------------------------------
/client/components/permissions/ManageGrantOfferedPermissionsTable.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import ManageGrantOfferedPermissionsTable from './ManageGrantOfferedPermissionsTable';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | data: [],
9 | deletePermissionRows: () => {},
10 | ...overrides
11 | });
12 |
13 | const wrapper = shallow();
14 |
15 | describe('(Component) ManageGrantOfferedPermissionsTable', () => {
16 | it('renders without exploding', () => {
17 | expect(wrapper).to.have.lengthOf(1);
18 | });
19 | });
--------------------------------------------------------------------------------
/client/components/permissions/ManageGrantedPermissionsTable.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import ManageGrantedPermissionsTable from './ManageGrantedPermissionsTable';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | data: [],
9 | deletePermissionRows: () => {},
10 | ...overrides
11 | });
12 |
13 | const wrapper = shallow();
14 |
15 | describe('(Component) ManageGrantedPermissionsTable', () => {
16 | it('renders without exploding', () => {
17 | expect(wrapper).to.have.lengthOf(1);
18 | });
19 | });
--------------------------------------------------------------------------------
/client/components/permissions/ManageReceivedPermissionOffersTable.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import ManageReceivedPermissionOffersTable from './ManageReceivedPermissionOffersTable';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | data: [],
9 | rejectRows: () => {},
10 | acceptRows: () => {},
11 | ...overrides
12 | });
13 |
14 | const wrapper = shallow();
15 |
16 | describe('(Component) ManageReceivedPermissionOffersTable', () => {
17 | it('renders without exploding', () => {
18 | expect(wrapper).to.have.lengthOf(1);
19 | });
20 | });
--------------------------------------------------------------------------------
/client/components/templates/CreateTemplateForm.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import CreateTemplateForm from './CreateTemplateForm';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | touch: () => {},
9 | valid: true,
10 | pristine: true,
11 | submitting: true,
12 | nextPage: () => {},
13 | reset: () => {},
14 | validationFailed: () => {},
15 | textEditorType: '',
16 | passResetToState: () => {},
17 | ...overrides
18 | });
19 |
20 | const wrapper = shallow();
21 |
22 | describe('(Component) CreateTemplateForm', () => {
23 | it('renders without exploding', () => {
24 | expect(wrapper).to.have.lengthOf(1);
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/client/components/templates/ManageTemplatesTable.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import ManageTemplatesTable from './ManageTemplatesTable';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | data: [],
9 | deleteRows: () => {},
10 | getTemplateView: () => {},
11 | ...overrides
12 | });
13 |
14 | const wrapper = shallow();
15 |
16 | describe('(Component) ManageTemplatesTable', () => {
17 | it('renders without exploding', () => {
18 | expect(wrapper).to.have.lengthOf(1);
19 | });
20 | });
--------------------------------------------------------------------------------
/client/components/templates/PreviewTemplateForm.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import PreviewTemplateForm from './PreviewTemplateForm';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | handleSubmit: () => {},
9 | lastPage: () => {},
10 | form: { values: { emailBody: '' } },
11 | templateView: {},
12 | submitting: true,
13 | ...overrides
14 | });
15 |
16 | const wrapper = shallow();
17 |
18 | describe('(Component) PreviewTemplateForm', () => {
19 | it('renders without exploding', () => {
20 | expect(wrapper).to.have.lengthOf(1);
21 | });
22 | });
--------------------------------------------------------------------------------
/client/containers/AddEmail.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import { AddEmailComponent } from './AddEmail';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | addSubscribers: () => {}
9 | });
10 |
11 | const wrapper = shallow();
12 |
13 | describe('(Container) AddEmail', () => {
14 | it('renders without exploding', () => {
15 | expect(wrapper).to.have.lengthOf(1);
16 | });
17 | });
--------------------------------------------------------------------------------
/client/containers/App.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import { AppComponent } from './App';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | children: ,
9 | user: {},
10 | ws_notification: [],
11 | isGettingActivePermissions: true,
12 | activePermissionsEmails: [],
13 | accountForm: {},
14 | activeAccount: {},
15 | emitProfileRequest: () => {},
16 | consumeNotification: () => {},
17 | getActivePermissions: () => {},
18 | becomeAnotherUser: () => {},
19 | becomeSelf: () => {},
20 | route: {},
21 | location: { pathname: '' }
22 | });
23 |
24 | const mockContext = {
25 | router: { isActive: (a, b) => true } // eslint-disable-line no-unused-vars
26 | };
27 |
28 | const wrapper = shallow(, { context: mockContext });
29 |
30 | describe('(Container) App', () => {
31 | it('renders without exploding', () => {
32 | expect(wrapper).to.have.lengthOf(1);
33 | });
34 | });
--------------------------------------------------------------------------------
/client/containers/Dashboard.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import { DashboardComponent } from './Dashboard';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | children: ,
9 | user: {},
10 | campaigns: []
11 | });
12 |
13 | const wrapper = shallow();
14 |
15 | describe('(Container) Dashboard', () => {
16 | it('renders without exploding', () => {
17 | expect(wrapper).to.have.lengthOf(1);
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/client/containers/Notifications.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import { NotificationsComponent } from './Notifications';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | notifications: [],
9 | consume: () => {}
10 | });
11 |
12 | const wrapper = shallow();
13 |
14 | describe('(Container) Notifications', () => {
15 | it('renders without exploding', () => {
16 | expect(wrapper).to.have.lengthOf(1);
17 | });
18 | });
--------------------------------------------------------------------------------
/client/containers/Settings.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import { SettingsComponent } from './Settings';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | getBooleanForAssignedSettings: () => {},
9 | changeSettings: () => {},
10 | notify: () => {},
11 | loading: true,
12 | fieldsExist: {},
13 | touch: () => {},
14 | valid: true,
15 | pristine: true,
16 | submitting: true,
17 | reset: () => {}
18 | });
19 |
20 | const wrapper = shallow();
21 |
22 | describe('(Container) Settings', () => {
23 | it('renders without exploding', () => {
24 | expect(wrapper).to.have.lengthOf(1);
25 | });
26 | });
--------------------------------------------------------------------------------
/client/containers/analytics/CampaignReports.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import FontAwesome from 'react-fontawesome';
3 | import CampaignReportsTable from '../../components/analytics/CampaignReportsTable';
4 |
5 | export default class CampaignReports extends Component {
6 |
7 | static propTypes = {
8 | isGetting: PropTypes.bool
9 | }
10 |
11 | state = {
12 | data: []
13 | }
14 |
15 | render() {
16 | return (
17 |
18 |
19 |
Manage campaigns
20 | Edit or delete your campaigns here
21 |
22 |
23 |
24 |
25 |
26 |
27 |
Your campaigns
28 |
29 |
30 |
31 |
32 |
33 |
34 | {this.props.isGetting &&
35 |
36 |
}
37 |
38 |
39 |
40 |
41 | );
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/client/containers/analytics/CampaignReports.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import CampaignReports from './CampaignReports';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | isGetting: true
9 | });
10 |
11 | const wrapper = shallow();
12 |
13 | describe('(Container) CampaignReports', () => {
14 | it('renders without exploding', () => {
15 | expect(wrapper).to.have.lengthOf(1);
16 | });
17 | });
--------------------------------------------------------------------------------
/client/containers/campaigns/CampaignView.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import { CampaignViewComponent } from './CampaignView';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | postSendCampaign: () => {},
9 | postTestEmail: () => {},
10 | getCampaigns: () => {},
11 | stopSending: () => {},
12 | notify: () => {},
13 | campaigns: [{ slug: 'mockSlug', totalCampaignSubscribers: 0 }],
14 | isGetting: true,
15 | sendCampaign: () => {},
16 | isPostingSendCampaign: true,
17 | sendCampaignResponse: '',
18 | sendCampaignStatus: 0,
19 | isPostingSendTest: true,
20 | sendTestEmailResponse: '',
21 | sendTestEmailStatus: 0,
22 | params: { slug: 'mockSlug'}
23 | });
24 |
25 | const wrapper = shallow();
26 |
27 | describe('(Container) CampaignView', () => {
28 | it('renders without exploding', () => {
29 | expect(wrapper).to.have.lengthOf(1);
30 | });
31 | });
--------------------------------------------------------------------------------
/client/containers/campaigns/CreateCampaign.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import { CreateCampaignComponent } from './CreateCampaign';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | form: { values: {} },
9 | isPosting: true,
10 | postCreateCampaign: () => {},
11 | getLists: () => {},
12 | lists: [],
13 | isGetting: true,
14 | getTemplates: () => {},
15 | templates: [],
16 | initialize: () => {},
17 | notify: () => {}
18 | });
19 |
20 | const mockContext = {
21 | router: { isActive: (a, b) => true } // eslint-disable-line no-unused-vars
22 | };
23 |
24 | const wrapper = shallow(, { context: mockContext });
25 |
26 | describe('(Container) CreateCampaign', () => {
27 | it('renders without exploding', () => {
28 | expect(wrapper).to.have.lengthOf(1);
29 | });
30 | });
--------------------------------------------------------------------------------
/client/containers/campaigns/ManageCampaignsBox.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import { ManageCampaignsBoxComponent } from './ManageCampaignsBox';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | campaigns: [],
9 | isGetting: true,
10 | getCampaigns: () => {},
11 | deleteCampaigns: () => {}
12 | });
13 |
14 | const mockContext = {
15 | router: { isActive: (a, b) => true } // eslint-disable-line no-unused-vars
16 | };
17 |
18 | const wrapper = shallow(, { context: mockContext });
19 |
20 | describe('(Container) ManageCampaignsBox', () => {
21 | it('renders without exploding', () => {
22 | expect(wrapper).to.have.lengthOf(1);
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/client/containers/common/TextEditor.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react';
2 | import 'react-quill/dist/quill.snow.css';
3 |
4 | import TextEditorRich from '../../components/common/TextEditorRich';
5 | import TextEditorPlain from '../../components/common/TextEditorPlain';
6 |
7 | export default class TextEditor extends Component {
8 |
9 | static propTypes = {
10 | input: PropTypes.object.isRequired,
11 | textEditorType: PropTypes.string
12 | }
13 |
14 | constructor(props) {
15 | super(props);
16 | this.onChange = this.onChange.bind(this);
17 | }
18 |
19 | onChange(value) {
20 | // Update redux form
21 | this.props.input.onChange(value);
22 | }
23 |
24 | render() {
25 | const {
26 | input: {
27 | value
28 | }
29 | } = this.props;
30 |
31 | const isPlaintext = this.props.textEditorType === 'Plaintext';
32 |
33 | const textEditorProps = {
34 | value,
35 | onChange: this.onChange
36 | };
37 |
38 | // Render either a plaintext or html editor
39 | return (
40 | isPlaintext
41 | ?
42 | :
43 | );
44 | }
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/client/containers/common/TextEditor.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import TextEditor from './TextEditor';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | input: {},
9 | textEditorType: ''
10 | });
11 |
12 | const wrapper = shallow();
13 |
14 | describe('(Container) TextEditor', () => {
15 | it('renders without exploding', () => {
16 | expect(wrapper).to.have.lengthOf(1);
17 | });
18 | });
--------------------------------------------------------------------------------
/client/containers/lists/CreateList.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import { CreateListComponent } from './CreateList';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | submitCSV: () => {},
9 | notify: () => {},
10 | lists: [],
11 | isGetting: true
12 | });
13 |
14 | const mockContext = {
15 | router: { isActive: (a, b) => true } // eslint-disable-line no-unused-vars
16 | };
17 |
18 | const wrapper = shallow(, { context: mockContext });
19 |
20 | describe('(Container) CreateList', () => {
21 | it('renders without exploding', () => {
22 | expect(wrapper).to.have.lengthOf(1);
23 | });
24 | });
--------------------------------------------------------------------------------
/client/containers/lists/ImportCSV.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import { ImportCSVComponent } from './ImportCSV';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | handleCSVSubmit: () => {},
9 | isPosting: true,
10 | notification: () => {},
11 | upload: {}
12 | });
13 |
14 | const wrapper = shallow();
15 |
16 | describe('(Container) ImportCSV', () => {
17 | it('renders without exploding', () => {
18 | expect(wrapper).to.have.lengthOf(1);
19 | });
20 | });
--------------------------------------------------------------------------------
/client/containers/lists/ManageListSubscribers.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import { ManageListSubscribersComponent } from './ManageListSubscribers';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | subscribers: [],
9 | isGetting: true,
10 | deleteListSubscribers: () => {},
11 | getListSubscribers: () => {},
12 | params: { listId: 0 },
13 | totalListSubscribers: 0,
14 | additionalFields: []
15 | });
16 |
17 | const wrapper = shallow();
18 |
19 | describe('(Container) ManageListSubscribers', () => {
20 | it('renders without exploding', () => {
21 | expect(wrapper).to.have.lengthOf(1);
22 | });
23 | });
--------------------------------------------------------------------------------
/client/containers/lists/ManageListsBox.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import { ManageListsBoxComponent } from './ManageListsBox';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | getLists: () => {},
9 | lists: [],
10 | isGetting: true,
11 | deleteLists: () => {}
12 | });
13 |
14 | const wrapper = shallow();
15 |
16 | describe('(Container) ManageListsBox', () => {
17 | it('renders without exploding', () => {
18 | expect(wrapper).to.have.lengthOf(1);
19 | });
20 | });
--------------------------------------------------------------------------------
/client/containers/permissions/GrantPermissions.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import { GrantPermissionsComponent } from './GrantPermissions';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | form: {},
9 | isPosting: true,
10 | response: {},
11 | notify: () => {},
12 | postGrantPermission: () => {}
13 | });
14 |
15 | const wrapper = shallow();
16 |
17 | describe('(Container) GrantPermissions', () => {
18 | it('renders without exploding', () => {
19 | expect(wrapper).to.have.lengthOf(1);
20 | });
21 | });
--------------------------------------------------------------------------------
/client/containers/permissions/OfferedPermissions.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import { OfferedPermissionsComponent } from './OfferedPermissions';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | isGettingGrantedPermissions: true,
9 | grantedPermissions: [],
10 | isGettingGrantOfferedPermissions: true,
11 | grantOfferedPermissions: [],
12 | getGrantPermissions: () => {},
13 | deleteGrantedPermissions: () => {},
14 | getGrantOfferedPermissions: () => {},
15 | deleteGrantOfferedPermissions: () => {},
16 | notify: () => {}
17 | });
18 |
19 | const wrapper = shallow();
20 |
21 | describe('(Container) OfferedPermissions', () => {
22 | it('renders without exploding', () => {
23 | expect(wrapper).to.have.lengthOf(1);
24 | });
25 | });
--------------------------------------------------------------------------------
/client/containers/permissions/ReceivedPermissions.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import { ReceivedPermissionsComponent } from './ReceivedPermissions';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | isGettingReceivedPermissionOffers: true,
9 | receivedPermissionOffers: [],
10 | isGettingActivePermissions: true,
11 | activePermissions: [],
12 | getReceivedPermissionOffers: () => {},
13 | deleteRejectReceivedOffers: () => {},
14 | getActivePermissions: () => {},
15 | deleteActivePermissions: () => {},
16 | postAcceptReceivedOffers: () => {},
17 | notify: () => {}
18 | });
19 |
20 | const wrapper = shallow();
21 |
22 | describe('(Container) ReceivedPermissions', () => {
23 | it('renders without exploding', () => {
24 | expect(wrapper).to.have.lengthOf(1);
25 | });
26 | });
--------------------------------------------------------------------------------
/client/containers/templates/CreateTemplate.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import { CreateTemplateComponent } from './CreateTemplate';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | form: { values: {} },
9 | isPosting: true,
10 | postCreateTemplate: () => {},
11 | templates: [],
12 | isGetting: true,
13 | notify: () => {}
14 | });
15 |
16 | const wrapper = shallow();
17 |
18 | describe('(Container) CreateTemplate', () => {
19 | it('renders without exploding', () => {
20 | expect(wrapper).to.have.lengthOf(1);
21 | });
22 | });
--------------------------------------------------------------------------------
/client/containers/templates/ManageTemplates.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import { ManageTemplatesComponent } from './ManageTemplates';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | form: {},
9 | getTemplates: () => {},
10 | templates: [],
11 | isGetting: true,
12 | deleteTemplates: () => {},
13 | notify: () => {}
14 | });
15 |
16 |
17 | const mockContext = {
18 | router: { isActive: (a, b) => true } // eslint-disable-line no-unused-vars
19 | };
20 |
21 | const wrapper = shallow(, { context: mockContext });
22 |
23 | describe('(Container) ManageTemplates', () => {
24 | it('renders without exploding', () => {
25 | expect(wrapper).to.have.lengthOf(1);
26 | });
27 | });
--------------------------------------------------------------------------------
/client/containers/templates/TemplateView.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import { TemplateViewComponent } from './TemplateView';
6 |
7 | const mockProps = ( overrides) => ({ // eslint-disable-line no-unused-vars
8 | notify: () => {},
9 | getTemplates: () => {},
10 | templates: [{ slug: 'mockSlug' }],
11 | isGetting: true,
12 | params: { slug: 'mockSlug' }
13 | });
14 |
15 | const wrapper = shallow();
16 |
17 | describe('(Container) TemplateView', () => {
18 | it('renders without exploding', () => {
19 | expect(wrapper).to.have.lengthOf(1);
20 | });
21 | });
--------------------------------------------------------------------------------
/client/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/client/favicon.ico
--------------------------------------------------------------------------------
/client/reducers/accountsManagementReducer.js:
--------------------------------------------------------------------------------
1 | import initialState from './initialState';
2 | import {
3 | REQUEST_POST_CREATE_NEW_USER, COMPLETE_POST_CREATE_NEW_USER,
4 | REQUEST_DELETE_USER, COMPLETE_DELETE_USER,
5 | } from '../constants/actionTypes';
6 |
7 | export function createAccount(state = initialState.createAccount, action) {
8 | switch(action.type) {
9 | case REQUEST_POST_CREATE_NEW_USER: {
10 | return {...state,
11 | isGetting:true
12 | };
13 | }
14 | case COMPLETE_POST_CREATE_NEW_USER: {
15 | return {...state,
16 | isGetting:false
17 | };
18 | }
19 | default:
20 | return state;
21 | }
22 | }
23 |
24 | export function deleteAccount(state = initialState.deleteAccount, action) {
25 | switch(action.type) {
26 | case REQUEST_DELETE_USER: {
27 | return {...state,
28 | isGetting:true
29 | };
30 | }
31 | case COMPLETE_DELETE_USER: {
32 | return {...state,
33 | isGetting:false
34 | };
35 | }
36 | default:
37 | return state;
38 | }
39 | }
40 |
41 | export default {
42 | createAccount,
43 | deleteAccount
44 | };
45 |
--------------------------------------------------------------------------------
/client/reducers/appReducer.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 |
3 | import {
4 | requestProfile,
5 | completeProfile,
6 | receiveNotification,
7 | consumeNotification
8 | } from '../actions/appActions';
9 |
10 | import initialState from './initialState';
11 | import { profile } from './appReducer';
12 |
13 | describe('(Reducer/Action Creator) app', () => {
14 |
15 | // profile reducer
16 |
17 | it('should handle REQUEST_WS_PROFILE', () => {
18 | expect(
19 | profile(undefined, requestProfile())
20 | ).to.deep.equal({
21 | ...initialState.profile
22 | });
23 | });
24 |
25 | it('should handle COMPLETE_WS_PROFILE', () => {
26 | const mockData = { something: 'something' };
27 | expect(
28 | profile(undefined, completeProfile(mockData))
29 | ).to.deep.equal({
30 | ...initialState.profile,
31 | user: { ...mockData }
32 | });
33 | });
34 |
35 | // it('should handle RECEIVE_WS_NOTIFICATION', () => {}):
36 |
37 | // it('should handle CONSUME_WS_NOTIFICATION', () => {});
38 |
39 | });
--------------------------------------------------------------------------------
/client/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import { routerReducer } from 'react-router-redux';
3 | import { reducer as formReducer } from 'redux-form';
4 |
5 | import { createCampaign, createTemplate, manageCampaign, manageTemplates, sendCampaign, sendTest } from './campaignReducer';
6 | import { createList, manageList, manageListSubscribers } from './listReducer';
7 | import { profile } from './appReducer';
8 | import settings from './settingsReducer';
9 | import notifications from './notificationsReducer';
10 | import { grantPermissions, receivedPermissionOffers, activePermissions, grantOfferedPermissions, activeAccount } from './permissionReducer';
11 | import { createAccount ,deleteAccount } from './accountsManagementReducer'
12 |
13 | const rootReducer = combineReducers({
14 | createCampaign,
15 | createTemplate,
16 | createAccount,
17 | deleteAccount,
18 | manageCampaign,
19 | manageTemplates,
20 | sendCampaign,
21 | sendTest,
22 | createList,
23 | manageList,
24 | manageListSubscribers,
25 | settings,
26 | notifications,
27 | profile,
28 | grantPermissions,
29 | receivedPermissionOffers,
30 | activePermissions,
31 | grantOfferedPermissions,
32 | activeAccount,
33 | routing: routerReducer,
34 | form: formReducer
35 | });
36 |
37 | export default rootReducer;
38 |
--------------------------------------------------------------------------------
/client/reducers/notificationsReducer.js:
--------------------------------------------------------------------------------
1 | import initialState from './initialState';
2 | import { RECEIVE_NOTIFICATION, CONSUME_NOTIFICATION } from '../constants/actionTypes';
3 |
4 |
5 | export default function notifications(state = initialState.notifications, action) {
6 | switch (action.type) {
7 | case RECEIVE_NOTIFICATION: {
8 | return {...state,
9 | stack: state.stack.concat(action.notification)
10 | };
11 | }
12 | case CONSUME_NOTIFICATION: {
13 | return {...state,
14 | stack: state.stack.slice(1)
15 | };
16 | }
17 | default:
18 | return state;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/client/reducers/notificationsReducer.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 |
3 | import {
4 | notify,
5 | consume
6 | } from '../actions/notificationActions';
7 |
8 | import initialState from './initialState';
9 | import notifications from './notificationsReducer';
10 |
11 | describe('(Reducer/Action Creator) notification', () => {
12 |
13 | // notifications reducer
14 |
15 | it('should handle RECEIVE_NOTIFICATION', () => {
16 | const mockNotification = { message: 'something1', colour: 'something2'};
17 | const mockStackElement = {
18 | message: mockNotification.message,
19 | dismissAfter: 20000,
20 | isActive: true,
21 | activeBarStyle: {
22 | background: 'crimson',
23 | left: ''
24 | }
25 | };
26 | expect(
27 | notifications(undefined, notify(mockNotification))
28 | ).to.deep.equal({
29 | ...initialState.notifications,
30 | stack: [ ...initialState.notifications.stack, mockStackElement ]
31 | });
32 | });
33 |
34 | it('should handle RECEIVE_NOTIFICATION', () => {
35 | expect(
36 | notifications(undefined, consume())
37 | ).to.deep.equal({
38 | ...initialState.notifications,
39 | stack: initialState.notifications.stack.slice(1)
40 | });
41 | });
42 |
43 | });
--------------------------------------------------------------------------------
/client/reducers/settingsReducer.js:
--------------------------------------------------------------------------------
1 | import initialState from './initialState';
2 | import { SETTINGS_CHANGE_RECEIVE, SETTINGS_CHANGE_REQUEST, SETTINGS_UPDATE_FIELDS_EXIST } from '../constants/actionTypes';
3 |
4 |
5 | export default function settings(state = initialState.settings, action) {
6 | switch (action.type) {
7 | case SETTINGS_CHANGE_REQUEST: {
8 | return {...state,
9 | loading: true
10 | };
11 | }
12 | case SETTINGS_CHANGE_RECEIVE: {
13 | return {...state,
14 | loading: false,
15 | status: action.payload.status
16 | };
17 | }
18 | case SETTINGS_UPDATE_FIELDS_EXIST: {
19 | return {...state,
20 | fieldsExist: action.payload
21 | };
22 | }
23 | default:
24 | return state;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/client/reducers/settingsReducer.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 |
3 | import {
4 | requestChangeSettings,
5 | receiveChangeSettings,
6 | updateSettingsFieldsExist
7 | } from '../actions/settingsActions';
8 |
9 | import initialState from './initialState';
10 |
11 | import settings from './settingsReducer';
12 |
13 | describe('(Reducer/Action Creator) settings', () => {
14 |
15 | // settings reducer
16 |
17 | it('should handle SETTINGS_CHANGE_REQUEST', () => {
18 | expect(
19 | settings(undefined, requestChangeSettings())
20 | ).to.deep.equal({
21 | ...initialState.settings,
22 | loading: true
23 | });
24 | });
25 |
26 | it('should handle SETTINGS_CHANGE_RECEIVE', () => {
27 | const mockStatus = { something: 'something' };
28 | expect(
29 | settings(undefined, receiveChangeSettings(mockStatus))
30 | ).to.deep.equal({
31 | ...initialState.settings,
32 | loading: false,
33 | status: ''
34 | });
35 | });
36 |
37 | it('should handle SETTINGS_UPDATE_FIELDS_EXIST', () => {
38 | const mockPayload = 'something';
39 | expect(
40 | settings(undefined, updateSettingsFieldsExist(mockPayload))
41 | ).to.deep.equal({
42 | ...initialState.settings,
43 | fieldsExist: mockPayload
44 | });
45 | });
46 |
47 | });
--------------------------------------------------------------------------------
/client/store/configureStore.js:
--------------------------------------------------------------------------------
1 | if (process.env.NODE_ENV === 'production') {
2 | module.exports = require('./configureStore.prod');
3 | } else {
4 | module.exports = require('./configureStore.dev');
5 | }
6 |
--------------------------------------------------------------------------------
/client/store/configureStore.prod.js:
--------------------------------------------------------------------------------
1 | import { createStore, compose, applyMiddleware } from 'redux';
2 | import persistState from 'redux-localstorage';
3 | import thunkMiddleware from 'redux-thunk';
4 | import rootReducer from '../reducers';
5 |
6 | export default function configureStore(initialState) {
7 | const middewares = [
8 | thunkMiddleware
9 | ];
10 |
11 | return createStore(rootReducer, initialState, compose(
12 | applyMiddleware(...middewares),
13 | // Save subset of state to local storage
14 | persistState(['activeAccount'])
15 | )
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/client/styles/header.scss:
--------------------------------------------------------------------------------
1 | .skin-green .main-header .logo {
2 | background-color: #007E00 !important;
3 | }
4 |
--------------------------------------------------------------------------------
/client/styles/index.scss:
--------------------------------------------------------------------------------
1 | @import './tables.scss';
2 |
3 | // Override default AdminLTE values
4 | @font-face
5 | {
6 | font-family: Lato;
7 | src: url('../utils/adminLte/assets/Lato-Regular.ttf');
8 | }
9 |
10 | html, body, h1, h2, h3, h4, h5, h6, p {
11 | font-family: Lato, sans-serif!important;
12 | }
13 |
14 | // Ensure the text editor is at least 60vh
15 | .TextEditor {
16 | min-height: 60vh;
17 | }
18 |
19 | .ql-editor {
20 | min-height: 60vh;
21 | }
22 |
23 | // Override Bootstrap table's orange 'delete' button colour
24 | .react-bs-table-del-btn {
25 | color: #fff;
26 | background-color: #d9534f;
27 | border-color: #d43f3a;
28 | }
29 |
30 | .btn-hug {
31 | margin: 1em;
32 | width: 125px;
33 | }
34 |
--------------------------------------------------------------------------------
/client/styles/tables.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/client/styles/tables.scss
--------------------------------------------------------------------------------
/client/utils/adminLte/assets/Lato-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/client/utils/adminLte/assets/Lato-Regular.ttf
--------------------------------------------------------------------------------
/client/utils/deepFillArray.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | export default function deepFillArray (size, element) {
4 | // Fill an array of ->size with deep cloned ->element(s)
5 | let array = [];
6 | for (let i = 0; i < size; i++ ) {
7 | array.push(_.cloneDeep(element));
8 | }
9 |
10 | return array;
11 | }
12 |
--------------------------------------------------------------------------------
/client/utils/subscriberListParsers/parseSubscriberList.js:
--------------------------------------------------------------------------------
1 | import Papa from 'papaparse';
2 |
3 | export default function previewCSV(file, callback) {
4 | // Parses a file (tsv/csv) into an object containing subscribers
5 | // and their fields
6 | Papa.parse(file, {
7 | header: true, // First row are headers
8 | preview: 10, // Show first 10 results
9 | skipEmptyLines: true,
10 | complete: function(results) { // Can also take 2nd 'file' parameter
11 | // Filter results to get rid of empty arrays. This occurs when files with less than 10 (or 11) rows are provided.
12 | results.data = results.data.filter(i => i.email !== '');
13 | callback(results);
14 | }
15 | });
16 | }
17 |
--------------------------------------------------------------------------------
/client/webpack-public-path.js:
--------------------------------------------------------------------------------
1 | // Dynamically set the webpack public path at runtime below
2 | // This magic global is used by webpack to set the public path at runtime.
3 | // The public path is set dynamically to avoid the following issues:
4 | // 1. https://github.com/coryhouse/react-slingshot/issues/205
5 | // 2. https://github.com/coryhouse/react-slingshot/issues/181
6 | // 3. https://github.com/coryhouse/react-slingshot/pull/125
7 | // Documentation: http://webpack.github.io/docs/configuration.html#output-publicpath
8 | // eslint-disable-next-line no-undef
9 | __webpack_public_path__ = window.location.protocol + "//" + window.location.host + "/";
10 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | postgres:
2 | image: postgres:9.6.3
3 | redis:
4 | image: redis:4.0
5 | app:
6 | image: freecodecamp/mail-for-good:stable
7 | env_file: .env
8 | environment:
9 | - PSQL_USERNAME=postgres
10 | - PSQL_DATABASE=postgres
11 | - PSQL_HOST=postgres
12 | - REDIS_HOST=redis
13 | - VIRTUAL_HOST=app
14 | - VIRTUAL_PORT=8080
15 | links:
16 | - redis
17 | - postgres
18 | ports:
19 | - "80:8080"
20 |
--------------------------------------------------------------------------------
/docs/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/1.png
--------------------------------------------------------------------------------
/docs/10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/10.png
--------------------------------------------------------------------------------
/docs/11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/11.png
--------------------------------------------------------------------------------
/docs/12.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/12.png
--------------------------------------------------------------------------------
/docs/13.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/13.png
--------------------------------------------------------------------------------
/docs/14.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/14.png
--------------------------------------------------------------------------------
/docs/15.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/15.png
--------------------------------------------------------------------------------
/docs/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/2.png
--------------------------------------------------------------------------------
/docs/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/3.png
--------------------------------------------------------------------------------
/docs/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/4.png
--------------------------------------------------------------------------------
/docs/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/5.png
--------------------------------------------------------------------------------
/docs/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/6.png
--------------------------------------------------------------------------------
/docs/7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/7.png
--------------------------------------------------------------------------------
/docs/8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/8.png
--------------------------------------------------------------------------------
/docs/9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/9.png
--------------------------------------------------------------------------------
/docs/categories/index.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Categories on Mail For Good
5 | https://freeCodeCamp.github.io/mail-for-good/categories/
6 | Recent content in Categories on Mail For Good
7 | Hugo -- gohugo.io
8 | en-us
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/docs/create_campaign_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/create_campaign_1.png
--------------------------------------------------------------------------------
/docs/create_campaign_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/create_campaign_2.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/archetypes/default.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "{{ replace .TranslationBaseName "-" " " | title }}"
3 | date: {{ .Date }}
4 | draft: true
5 | ---
6 |
7 |
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/getting-started/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Getting Started"
3 | date: 2017-09-30T16:08:39-05:00
4 | ---
5 |
6 | ## How to install
7 |
8 | Get the app up and running by following our [**AWS deployment guide**](/aws-deployment-guide). After following these steps watch [**this guide**](https://www.youtube.com/watch?v=_7U03GVD4a8) to set up the keys you'll need for sending emails.
9 |
10 | Want to test the app on your own machine? Check out our [**local deployment guide**](/local-deployment-guide).
11 |
12 | ## Troubleshooting
13 |
14 | We're keen to tackle any issues people encounter. If you experience any problems, please create an issue and we'll get back to you.
15 |
16 | If at any point you changed a file after running `docker-compose`, run `docker-compose up --force-recreate` to ensure they're included.
17 |
18 |
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/google-api-guide.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "How to configure your Google API Keys"
3 | date: 2017-09-30T16:56:37-05:00
4 | ---
5 |
6 | 1. Login to [Google API Manager](https://console.developers.google.com/apis/).
7 | 2. In the left menu, select **Dashboard**. Now select **Enable API**, search for `Google+` and select it. At the top of the screen, ensure it's enabled by clicking on **Enable**.
8 | 3. In the left menu, select **Credentials**. Then click **Create Credentials** > **OAuth client ID**.
9 | 4. Select **Web Application**. Name is as you wish, but under **Authorized Javascript Origins** put `http://localhost:8080`, and under **Authorized redirect URIs** put `http://localhost:8080/auth/google/callback`.
10 | 5. Click **Create**. You will now have a Client ID and Client Secret. In your .env file, put the Client ID as your GOOGLE_CONSUMER_KEY, and the Client Secret as your GOOGLE_CONSUMER_SECRET.
11 |
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/license/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "License"
3 | date: 2017-09-30T16:55:45-05:00
4 | ---
5 |
6 | Copyright (c) 2017, freeCodeCamp.
7 |
8 | This computer software is licensed under the [BSD-3-Clause](https://github.com/freeCodeCamp/Mail-for-Good/blob/master/LICENSE.md).
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/content/public/1.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/content/public/10.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/content/public/11.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/12.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/content/public/12.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/13.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/content/public/13.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/14.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/content/public/14.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/15.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/content/public/15.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/content/public/2.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/content/public/3.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/content/public/4.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/content/public/5.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/content/public/6.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/content/public/7.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/content/public/8.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/content/public/9.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/categories/index.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Categories on Mail For Good
5 | /categories/
6 | Recent content in Categories on Mail For Good
7 | Hugo -- gohugo.io
8 | en-us
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/create_campaign_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/content/public/create_campaign_1.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/create_campaign_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/content/public/create_campaign_2.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/elastic_allocate_new.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/content/public/elastic_allocate_new.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/elastic_associate.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/content/public/elastic_associate.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/elastic_associate_conf.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/content/public/elastic_associate_conf.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/fonts/icon.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/content/public/fonts/icon.eot
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/fonts/icon.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/content/public/fonts/icon.ttf
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/fonts/icon.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/content/public/fonts/icon.woff
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/google_origins.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/content/public/google_origins.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/header_logo.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/content/public/header_logo.jpeg
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/hero.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/content/public/hero.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/content/public/images/favicon.ico
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/content/public/images/logo.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/images/screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/content/public/images/screen.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/javascripts/test.js:
--------------------------------------------------------------------------------
1 | document.addEventListener('DOMContentLoaded', function () {
2 | console.log('loaded')
3 | })
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/license/index.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Licenses on Mail For Good
5 | /license/
6 | Recent content in Licenses on Mail For Good
7 | Hugo -- gohugo.io
8 | en-us
9 | Sat, 30 Sep 2017 16:55:45 -0500
10 |
11 |
12 |
13 |
14 | -
15 | License
16 | /license/
17 | Sat, 30 Sep 2017 16:55:45 -0500
18 |
19 | /license/
20 | Copyright © 2017, freeCodeCamp.
21 | This computer software is licensed under the BSD-3-Clause.
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/content/public/logo.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/modal.css:
--------------------------------------------------------------------------------
1 | @media (min-width: 960px) {
2 | #modal-wrap {
3 | display: none;
4 | position: fixed;
5 | top: 0;
6 | left: 0;
7 | right: 0;
8 | bottom: 0;
9 | background: rgba(0, 0, 0, .6);
10 | flex-direction: column;
11 | justify-content: center;
12 | align-items: center;
13 | }
14 |
15 | #modal {
16 | display: flex;
17 | flex-direction: column;
18 | justify-content: center;
19 | margin: 0 auto;
20 | width: 100%;
21 | }
22 |
23 | #modal-image {
24 | max-height: 70vh;
25 | margin: 5px auto;
26 | }
27 | #modal-close-button {
28 | display: flex;
29 | flex-direction: row;
30 | justify-content: flex-end;
31 | }
32 |
33 | #modal-close-button h2 {
34 | color: #fff;
35 | cursor: pointer;
36 | font-size: 50px;
37 | font-weight: bold;
38 | }
39 | }
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/navbar_ec2_elasticIP.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/content/public/navbar_ec2_elasticIP.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/content/public/settings.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/static/javascripts/modal.js:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Modal"
3 | date: 2017-11-06T07:40:51-06:00
4 | draft: true
5 | ---
6 |
7 |
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/static/stylesheets/modal.css:
--------------------------------------------------------------------------------
1 | --- title:"Modal" date:2017-11-06T07:41:14-06:00 --- @media(min-width:960px){#modal-wrap{display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.6);flex-direction:column;justify-content:center;align-items:center}#modal{display:flex;flex-direction:column;justify-content:center;margin:0 auto;width:100%}#modal-image{max-height:70vh;margin:5px auto}#modal-close-button{display:flex;flex-direction:row;justify-content:flex-end}#modal-close-button h2{color:#fff;cursor:pointer;font-size:50px;font-weight:bold}}
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/stylesheets/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | background: black;
3 | }
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/stylesheets/misc.css:
--------------------------------------------------------------------------------
1 | .wrapper:first-of-type h1{
2 | padding-bottom: 10px;
3 | margin-top: -40px;
4 | font-size: 36px;
5 | }
6 |
7 | @media (max-width: 450px) {
8 | .wrapper:first-of-type h1{
9 | font-size: 30px;
10 | }
11 | }
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/stylesheets/modal.css:
--------------------------------------------------------------------------------
1 | @media (min-width: 960px) {
2 | #modal-wrap {
3 | display: none;
4 | position: fixed;
5 | top: 0;
6 | left: 0;
7 | right: 0;
8 | bottom: 0;
9 | background: rgba(0, 0, 0, .6);
10 | flex-direction: column;
11 | justify-content: center;
12 | align-items: center;
13 | }
14 |
15 | #modal {
16 | display: flex;
17 | flex-direction: column;
18 | justify-content: center;
19 | margin: 0 auto;
20 | width: 100%;
21 | }
22 |
23 | #modal-image {
24 | max-height: 70vh;
25 | margin: 5px auto;
26 | }
27 | #modal-close-button {
28 | display: flex;
29 | flex-direction: row;
30 | justify-content: flex-end;
31 | }
32 |
33 | #modal-close-button p {
34 | color: #fff;
35 | cursor: pointer;
36 | font-size: 50px;
37 | font-weight: bold;
38 | margin-bottom: 20px;
39 | }
40 | }
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/stylesheets/temporary.css:
--------------------------------------------------------------------------------
1 | /* This file only exists (temporarily) until the
2 | custom styling can be replaced with the
3 | implementation of the upstream project.
4 | */
5 |
6 | blockquote {
7 | padding: 0 20px;
8 | margin: 0 0 20px;
9 | font-size: inherit;
10 | border-left: 5px solid #eee;
11 | }
12 |
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/public/tags/index.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Tags on Mail For Good
5 | /tags/
6 | Recent content in Tags on Mail For Good
7 | Hugo -- gohugo.io
8 | en-us
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/static/javascripts/modal.js:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Modal"
3 | date: 2017-11-06T07:40:51-06:00
4 | draft: true
5 | ---
6 |
7 |
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/content/static/stylesheets/modal.css:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Modal"
3 | date: 2017-11-06T07:41:14-06:00
4 | ---
5 |
6 | @media (min-width: 960px) {
7 | #modal-wrap {
8 | display: none;
9 | position: fixed;
10 | top: 0;
11 | left: 0;
12 | right: 0;
13 | bottom: 0;
14 | background: rgba(0, 0, 0, .6);
15 | flex-direction: column;
16 | justify-content: center;
17 | align-items: center;
18 | }
19 |
20 | #modal {
21 | display: flex;
22 | flex-direction: column;
23 | justify-content: center;
24 | margin: 0 auto;
25 | width: 100%;
26 | }
27 |
28 | #modal-image {
29 | max-height: 70vh;
30 | margin: 5px auto;
31 | }
32 | #modal-close-button {
33 | display: flex;
34 | flex-direction: row;
35 | justify-content: flex-end;
36 | }
37 |
38 | #modal-close-button h2 {
39 | color: #fff;
40 | cursor: pointer;
41 | font-size: 50px;
42 | font-weight: bold;
43 | }
44 | }
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/static/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/static/1.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/static/10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/static/10.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/static/11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/static/11.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/static/12.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/static/12.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/static/13.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/static/13.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/static/14.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/static/14.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/static/15.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/static/15.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/static/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/static/2.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/static/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/static/3.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/static/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/static/4.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/static/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/static/5.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/static/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/static/6.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/static/7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/static/7.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/static/8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/static/8.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/static/9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/static/9.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/static/create_campaign_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/static/create_campaign_1.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/static/create_campaign_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/static/create_campaign_2.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/static/elastic_allocate_new.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/static/elastic_allocate_new.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/static/elastic_associate.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/static/elastic_associate.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/static/elastic_associate_conf.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/static/elastic_associate_conf.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/static/google_origins.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/static/google_origins.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/static/header_logo.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/static/header_logo.jpeg
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/static/hero.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/static/hero.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/static/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/static/logo.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/static/navbar_ec2_elasticIP.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/static/navbar_ec2_elasticIP.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/static/settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/static/settings.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/static/stylesheets/misc.css:
--------------------------------------------------------------------------------
1 | .wrapper:first-of-type h1{
2 | padding-bottom: 10px;
3 | margin-top: -40px;
4 | font-size: 36px;
5 | }
6 |
7 | @media (max-width: 450px) {
8 | .wrapper:first-of-type h1{
9 | font-size: 30px;
10 | }
11 | }
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/static/stylesheets/modal.css:
--------------------------------------------------------------------------------
1 | @media (min-width: 960px) {
2 | #modal-wrap {
3 | display: none;
4 | position: fixed;
5 | top: 0;
6 | left: 0;
7 | right: 0;
8 | bottom: 0;
9 | background: rgba(0, 0, 0, .6);
10 | flex-direction: column;
11 | justify-content: center;
12 | align-items: center;
13 | }
14 |
15 | #modal {
16 | display: flex;
17 | flex-direction: column;
18 | justify-content: center;
19 | margin: 0 auto;
20 | width: 100%;
21 | }
22 |
23 | #modal-image {
24 | max-height: 70vh;
25 | margin: 5px auto;
26 | }
27 | #modal-close-button {
28 | display: flex;
29 | flex-direction: row;
30 | justify-content: flex-end;
31 | }
32 |
33 | #modal-close-button p {
34 | color: #fff;
35 | cursor: pointer;
36 | font-size: 50px;
37 | font-weight: bold;
38 | margin-bottom: 20px;
39 | }
40 | }
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/themes/hugo-material-docs/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 |
4 | ### 8th April 2017
5 |
6 | `.Now` has been deprecated. Hence the required minimum version of Hugo is v0.20.
7 |
8 | ### 11th May 2016
9 |
10 | #### Add templates for section lists
11 |
12 | Sections such as www.example.com/foo/ will now be rendered with a list of all pages that are part of this section. The list shows the pages' title and a summary of their content.
13 |
14 | [Show me the diff](https://github.com/digitalcraftsman/hugo-material-docs/commit/1f8393a8d4ce1b8ee3fc7d87be05895c12810494)
15 |
16 | ### 22nd March 2016
17 |
18 | #### Changing setup for Google Analytics
19 |
20 | Formerly, the tracking id for Google Analytics was set like below:
21 |
22 | ```toml
23 | [params]
24 | google_analytics = ["UA-XXXXXXXX-X", "auto"]
25 | ```
26 |
27 | Now the theme uses Hugo's own Google Analytics config option. The variable moved outside the scope of `params` and the setup requires only the tracking id as a string:
28 |
29 | ```toml
30 | googleAnalytics = "UA-XXXXXXXX-X"
31 | ```
32 |
33 | [Show me the diff](https://github.com/digitalcraftsman/hugo-material-docs/commit/fa10c8eef935932426d46b662a51f29a5e0d48e2)
34 |
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/themes/hugo-material-docs/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016 Digitalcraftsman
2 | Copyright (c) 2016 Martin Donath
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy
5 | of this software and associated documentation files (the "Software"), to
6 | deal in the Software without restriction, including without limitation the
7 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
8 | sell copies of the Software, and to permit persons to whom the Software is
9 | furnished to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in
12 | all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
20 | IN THE SOFTWARE.
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/themes/hugo-material-docs/archetypes/default.md:
--------------------------------------------------------------------------------
1 | ---
2 | ---
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/themes/hugo-material-docs/exampleSite/static/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/themes/hugo-material-docs/exampleSite/static/.gitkeep
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/themes/hugo-material-docs/images/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/themes/hugo-material-docs/images/screenshot.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/themes/hugo-material-docs/images/tn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/themes/hugo-material-docs/images/tn.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/themes/hugo-material-docs/layouts/404.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/themes/hugo-material-docs/layouts/404.html
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/themes/hugo-material-docs/layouts/_default/__list.html:
--------------------------------------------------------------------------------
1 | {{ partial "head" . }}
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
14 |
15 |
16 |
17 | {{ partial "drawer" . }}
18 |
19 |
20 |
21 |
22 |
Pages in {{ .Title | singularize }}
23 |
24 | {{ range .Data.Pages }}
25 |
26 | {{ .Title }}
27 |
28 |
29 |
30 | {{ printf "%s" .Summary | markdownify }}
31 |
32 |
33 | {{ end }}
34 |
35 |
36 |
37 |
45 |
46 |
47 | {{ partial "footer_js" . }}
48 |
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/themes/hugo-material-docs/layouts/partials/nav.html:
--------------------------------------------------------------------------------
1 | {{ $currentNode := . }}
2 |
3 | {{ range .Site.Menus.main.ByWeight }}
4 |
5 | {{ $.Scratch.Set "currentMenuEntry" . }}
6 |
7 | {{ if .HasChildren }}
8 | {{ .Name | title }}
9 |
10 | {{ range .Children }}
11 | {{ $.Scratch.Set "currentMenuEntry" . }}
12 | {{ partial "nav_link" $currentNode }}
13 | {{ end }}
14 |
15 | {{ else }}
16 | {{ partial "nav_link" $currentNode }}
17 | {{ end }}
18 |
19 | {{ end }}
20 |
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/themes/hugo-material-docs/layouts/partials/nav_link.html:
--------------------------------------------------------------------------------
1 | {{ $currentMenuEntry := .Scratch.Get "currentMenuEntry" }}
2 | {{ $isCurrent := eq .Permalink ($currentMenuEntry.URL | absURL | printf "%s") }}
3 |
4 |
5 |
6 | {{ $currentMenuEntry.Pre }}
7 | {{ $currentMenuEntry.Name }}
8 |
9 |
10 | {{ if $isCurrent }}
11 |
13 | {{ end }}
14 |
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/themes/hugo-material-docs/layouts/shortcodes/note.html:
--------------------------------------------------------------------------------
1 |
2 |
{{ .Get "title" }}
3 |
{{ printf "%s" .Inner | markdownify }}
4 |
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/themes/hugo-material-docs/layouts/shortcodes/warning.html:
--------------------------------------------------------------------------------
1 |
2 |
{{ .Get "title" }}
3 |
{{ printf "%s" .Inner | markdownify }}
4 |
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/themes/hugo-material-docs/static/fonts/icon.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/themes/hugo-material-docs/static/fonts/icon.eot
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/themes/hugo-material-docs/static/fonts/icon.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/themes/hugo-material-docs/static/fonts/icon.ttf
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/themes/hugo-material-docs/static/fonts/icon.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/themes/hugo-material-docs/static/fonts/icon.woff
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/themes/hugo-material-docs/static/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/themes/hugo-material-docs/static/images/favicon.ico
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/themes/hugo-material-docs/static/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/themes/hugo-material-docs/static/images/logo.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/themes/hugo-material-docs/static/images/screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/docs-hugo-templates/themes/hugo-material-docs/static/images/screen.png
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/themes/hugo-material-docs/static/stylesheets/highlight/highlight.css:
--------------------------------------------------------------------------------
1 | /*
2 | * overwrite the current primary color of the
3 | * theme that is used as fallback in codeblocks
4 | */
5 | .article pre code {
6 | color: rgba(0, 0, 0, 0.8) !important;
7 | font-size: 1.5rem;
8 | }
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/themes/hugo-material-docs/static/stylesheets/temporary.css:
--------------------------------------------------------------------------------
1 | /* This file only exists (temporarily) until the
2 | custom styling can be replaced with the
3 | implementation of the upstream project.
4 | */
5 |
6 | blockquote {
7 | padding: 0 20px;
8 | margin: 0 0 20px;
9 | font-size: inherit;
10 | border-left: 5px solid #eee;
11 | }
12 |
--------------------------------------------------------------------------------
/docs/docs-hugo-templates/themes/hugo-material-docs/theme.toml:
--------------------------------------------------------------------------------
1 | name = "Material Docs"
2 | license = "MIT"
3 | licenselink = "https://github.com/digitalcraftsman/hugo-material-docs/blob/master/LICENSE.md"
4 | description = "A material design theme for documentations."
5 | homepage = "https://github.com/digitalcraftsman/hugo-material-docs"
6 | tags = ["material", "documentation", "docs", "google analytics", "responsive"]
7 | features = []
8 | min_version = 0.20
9 |
10 | [author]
11 | name = "Digitalcraftsman"
12 | homepage = "https://github.com/digitalcraftsman"
13 |
14 | # If porting an existing theme
15 | [original]
16 | name = "Martin Donath"
17 | homepage = "http://struct.cc/"
18 | repo = "https://github.com/squidfunk/mkdocs-material"
19 |
--------------------------------------------------------------------------------
/docs/elastic_allocate_new.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/elastic_allocate_new.png
--------------------------------------------------------------------------------
/docs/elastic_associate.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/elastic_associate.png
--------------------------------------------------------------------------------
/docs/elastic_associate_conf.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/elastic_associate_conf.png
--------------------------------------------------------------------------------
/docs/fonts/icon.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/fonts/icon.eot
--------------------------------------------------------------------------------
/docs/fonts/icon.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/fonts/icon.ttf
--------------------------------------------------------------------------------
/docs/fonts/icon.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/fonts/icon.woff
--------------------------------------------------------------------------------
/docs/google_origins.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/google_origins.png
--------------------------------------------------------------------------------
/docs/header_logo.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/header_logo.jpeg
--------------------------------------------------------------------------------
/docs/hero.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/hero.png
--------------------------------------------------------------------------------
/docs/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/images/favicon.ico
--------------------------------------------------------------------------------
/docs/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/images/logo.png
--------------------------------------------------------------------------------
/docs/images/screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/images/screen.png
--------------------------------------------------------------------------------
/docs/license/index.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Licenses on Mail For Good
5 | https://freeCodeCamp.github.io/mail-for-good/license/
6 | Recent content in Licenses on Mail For Good
7 | Hugo -- gohugo.io
8 | en-us
9 | Sat, 30 Sep 2017 16:55:45 -0500
10 |
11 |
12 |
13 |
14 | -
15 | License
16 | https://freeCodeCamp.github.io/mail-for-good/license/
17 | Sat, 30 Sep 2017 16:55:45 -0500
18 |
19 | https://freeCodeCamp.github.io/mail-for-good/license/
20 | Copyright © 2017, freeCodeCamp.
21 | This computer software is licensed under the BSD-3-Clause.
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/docs/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/logo.png
--------------------------------------------------------------------------------
/docs/navbar_ec2_elasticIP.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/navbar_ec2_elasticIP.png
--------------------------------------------------------------------------------
/docs/planned_features.md:
--------------------------------------------------------------------------------
1 | # Planned features
2 |
3 | Below is a short, sure-to-change list of current priorities and planned features.
4 |
5 | Current:
6 |
7 | - Fix any issues encountered while using the application.
8 | - Streamline the build process to integrate HTTPS with certificates using Lets Encrypt should a public domain name exist. This entails the additional feature of using Nginx rather than the current port forward.
9 | - Whitelist users so that only certain users may use the app.
10 |
11 | Future additions:
12 |
13 | - A public API that can be securely accessed to perform actions such as sending individual emails or entire campaigns.
14 | - Inbound SMTP relay functionality.
15 |
--------------------------------------------------------------------------------
/docs/public/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/public/1.png
--------------------------------------------------------------------------------
/docs/public/10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/public/10.png
--------------------------------------------------------------------------------
/docs/public/11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/public/11.png
--------------------------------------------------------------------------------
/docs/public/12.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/public/12.png
--------------------------------------------------------------------------------
/docs/public/13.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/public/13.png
--------------------------------------------------------------------------------
/docs/public/14.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/public/14.png
--------------------------------------------------------------------------------
/docs/public/15.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/public/15.png
--------------------------------------------------------------------------------
/docs/public/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/public/2.png
--------------------------------------------------------------------------------
/docs/public/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/public/3.png
--------------------------------------------------------------------------------
/docs/public/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/public/4.png
--------------------------------------------------------------------------------
/docs/public/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/public/5.png
--------------------------------------------------------------------------------
/docs/public/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/public/6.png
--------------------------------------------------------------------------------
/docs/public/7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/public/7.png
--------------------------------------------------------------------------------
/docs/public/8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/public/8.png
--------------------------------------------------------------------------------
/docs/public/9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/public/9.png
--------------------------------------------------------------------------------
/docs/public/categories/index.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Categories on Mail For Good
5 | /categories/
6 | Recent content in Categories on Mail For Good
7 | Hugo -- gohugo.io
8 | en-us
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/docs/public/create_campaign_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/public/create_campaign_1.png
--------------------------------------------------------------------------------
/docs/public/create_campaign_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/public/create_campaign_2.png
--------------------------------------------------------------------------------
/docs/public/elastic_allocate_new.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/public/elastic_allocate_new.png
--------------------------------------------------------------------------------
/docs/public/elastic_associate.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/public/elastic_associate.png
--------------------------------------------------------------------------------
/docs/public/elastic_associate_conf.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/public/elastic_associate_conf.png
--------------------------------------------------------------------------------
/docs/public/fonts/icon.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/public/fonts/icon.eot
--------------------------------------------------------------------------------
/docs/public/fonts/icon.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/public/fonts/icon.ttf
--------------------------------------------------------------------------------
/docs/public/fonts/icon.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/public/fonts/icon.woff
--------------------------------------------------------------------------------
/docs/public/google_origins.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/public/google_origins.png
--------------------------------------------------------------------------------
/docs/public/header_logo.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/public/header_logo.jpeg
--------------------------------------------------------------------------------
/docs/public/hero.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/public/hero.png
--------------------------------------------------------------------------------
/docs/public/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/public/images/favicon.ico
--------------------------------------------------------------------------------
/docs/public/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/public/images/logo.png
--------------------------------------------------------------------------------
/docs/public/images/screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/public/images/screen.png
--------------------------------------------------------------------------------
/docs/public/javascripts/test.js:
--------------------------------------------------------------------------------
1 | document.addEventListener('DOMContentLoaded', function () {
2 | console.log('loaded')
3 | })
--------------------------------------------------------------------------------
/docs/public/license/index.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Licenses on Mail For Good
5 | /license/
6 | Recent content in Licenses on Mail For Good
7 | Hugo -- gohugo.io
8 | en-us
9 | Sat, 30 Sep 2017 16:55:45 -0500
10 |
11 |
12 |
13 |
14 | -
15 | License
16 | /license/
17 | Sat, 30 Sep 2017 16:55:45 -0500
18 |
19 | /license/
20 | Copyright © 2017, freeCodeCamp.
21 | This computer software is licensed under the BSD-3-Clause.
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/docs/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/public/logo.png
--------------------------------------------------------------------------------
/docs/public/modal.css:
--------------------------------------------------------------------------------
1 | @media(min-width:960px){#modal-wrap{display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.6);flex-direction:column;justify-content:center;align-items:center}#modal{display:flex;flex-direction:column;justify-content:center;margin:0 auto;width:100%}#modal-image{max-height:70vh;margin:5px auto}#modal-close-button{display:flex;flex-direction:row;justify-content:flex-end}#modal-close-button h2{color:#fff;cursor:pointer;font-size:50px;font-weight:bold}}
--------------------------------------------------------------------------------
/docs/public/navbar_ec2_elasticIP.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/public/navbar_ec2_elasticIP.png
--------------------------------------------------------------------------------
/docs/public/settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/public/settings.png
--------------------------------------------------------------------------------
/docs/public/static/javascripts/modal.js:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Modal"
3 | date: 2017-11-06T07:40:51-06:00
4 | draft: true
5 | ---
6 |
7 |
--------------------------------------------------------------------------------
/docs/public/static/stylesheets/modal.css:
--------------------------------------------------------------------------------
1 | --- title:"Modal" date:2017-11-06T07:41:14-06:00 --- @media(min-width:960px){#modal-wrap{display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.6);flex-direction:column;justify-content:center;align-items:center}#modal{display:flex;flex-direction:column;justify-content:center;margin:0 auto;width:100%}#modal-image{max-height:70vh;margin:5px auto}#modal-close-button{display:flex;flex-direction:row;justify-content:flex-end}#modal-close-button h2{color:#fff;cursor:pointer;font-size:50px;font-weight:bold}}
--------------------------------------------------------------------------------
/docs/public/stylesheets/highlight/github_highlightjs.css:
--------------------------------------------------------------------------------
1 | .hljs{display:block;overflow-x:auto;padding:.5em;color:#333;background:#f8f8f8}.hljs-comment,.hljs-quote{color:#998;font-style:italic}.hljs-keyword,.hljs-selector-tag,.hljs-subst{color:#333;font-weight:bold}.hljs-number,.hljs-literal,.hljs-variable,.hljs-template-variable,.hljs-tag .hljs-attr{color:#008080}.hljs-string,.hljs-doctag{color:#d14}.hljs-title,.hljs-section,.hljs-selector-id{color:#900;font-weight:bold}.hljs-subst{font-weight:normal}.hljs-type,.hljs-class .hljs-title{color:#458;font-weight:bold}.hljs-tag,.hljs-name,.hljs-attribute{color:#000080;font-weight:normal}.hljs-regexp,.hljs-link{color:#009926}.hljs-symbol,.hljs-bullet{color:#990073}.hljs-built_in,.hljs-builtin-name{color:#0086b3}.hljs-meta{color:#999;font-weight:bold}.hljs-deletion{background:#fdd}.hljs-addition{background:#dfd}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:bold}
--------------------------------------------------------------------------------
/docs/public/stylesheets/highlight/highlight.css:
--------------------------------------------------------------------------------
1 | .hljs{display:block;overflow-x:auto;padding:.5em;color:#333;background:#f8f8f8}.hljs-comment,.hljs-quote{color:#998;font-style:italic}.hljs-keyword,.hljs-selector-tag,.hljs-subst{color:#333}.hljs-number,.hljs-literal,.hljs-variable,.hljs-template-variable,.hljs-tag .hljs-attr{color:#008080}.hljs-string,.hljs-doctag{color:#d14}.hljs-title,.hljs-section,.hljs-selector-id{color:#900;font-weight:bold}.hljs-subst{font-weight:normal}.hljs-type,.hljs-class .hljs-title{color:#458;font-weight:bold}.hljs-tag,.hljs-name,.hljs-attribute{color:#000080;font-weight:normal}.hljs-regexp,.hljs-link{color:#009926}.hljs-symbol,.hljs-bullet{color:#990073}.hljs-built_in,.hljs-builtin-name{color:#0086b3}.hljs-meta{color:#999;font-weight:bold}.hljs-deletion{background:#fdd}.hljs-addition{background:#dfd}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:bold}
--------------------------------------------------------------------------------
/docs/public/stylesheets/index.css:
--------------------------------------------------------------------------------
1 | body{background:black}
--------------------------------------------------------------------------------
/docs/public/stylesheets/misc.css:
--------------------------------------------------------------------------------
1 | .wrapper:first-of-type h1{padding-bottom:10px;margin-top:-40px;font-size:36px}@media(max-width:450px){.wrapper:first-of-type h1{font-size:30px}}
--------------------------------------------------------------------------------
/docs/public/stylesheets/modal.css:
--------------------------------------------------------------------------------
1 | @media(min-width:960px){#modal-wrap{display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.6);flex-direction:column;justify-content:center;align-items:center}#modal{display:flex;flex-direction:column;justify-content:center;margin:0 auto;width:100%}#modal-image{max-height:70vh;margin:5px auto}#modal-close-button{display:flex;flex-direction:row;justify-content:flex-end}#modal-close-button p{color:#fff;cursor:pointer;font-size:50px;font-weight:bold;margin-bottom:20px}}
--------------------------------------------------------------------------------
/docs/public/stylesheets/temporary.css:
--------------------------------------------------------------------------------
1 | blockquote{padding:0 20px;margin:0 0 20px;font-size:inherit;border-left:5px solid #eee}
--------------------------------------------------------------------------------
/docs/public/tags/index.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Tags on Mail For Good
5 | /tags/
6 | Recent content in Tags on Mail For Good
7 | Hugo -- gohugo.io
8 | en-us
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/docs/resources/deploy_images/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/resources/deploy_images/1.png
--------------------------------------------------------------------------------
/docs/resources/deploy_images/10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/resources/deploy_images/10.png
--------------------------------------------------------------------------------
/docs/resources/deploy_images/11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/resources/deploy_images/11.png
--------------------------------------------------------------------------------
/docs/resources/deploy_images/12.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/resources/deploy_images/12.png
--------------------------------------------------------------------------------
/docs/resources/deploy_images/13.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/resources/deploy_images/13.png
--------------------------------------------------------------------------------
/docs/resources/deploy_images/14.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/resources/deploy_images/14.png
--------------------------------------------------------------------------------
/docs/resources/deploy_images/15.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/resources/deploy_images/15.png
--------------------------------------------------------------------------------
/docs/resources/deploy_images/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/resources/deploy_images/2.png
--------------------------------------------------------------------------------
/docs/resources/deploy_images/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/resources/deploy_images/3.png
--------------------------------------------------------------------------------
/docs/resources/deploy_images/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/resources/deploy_images/4.png
--------------------------------------------------------------------------------
/docs/resources/deploy_images/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/resources/deploy_images/5.png
--------------------------------------------------------------------------------
/docs/resources/deploy_images/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/resources/deploy_images/6.png
--------------------------------------------------------------------------------
/docs/resources/deploy_images/7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/resources/deploy_images/7.png
--------------------------------------------------------------------------------
/docs/resources/deploy_images/8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/resources/deploy_images/8.png
--------------------------------------------------------------------------------
/docs/resources/deploy_images/9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/resources/deploy_images/9.png
--------------------------------------------------------------------------------
/docs/resources/deploy_images/elastic_allocate_new.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/resources/deploy_images/elastic_allocate_new.png
--------------------------------------------------------------------------------
/docs/resources/deploy_images/elastic_associate.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/resources/deploy_images/elastic_associate.png
--------------------------------------------------------------------------------
/docs/resources/deploy_images/elastic_associate_conf.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/resources/deploy_images/elastic_associate_conf.png
--------------------------------------------------------------------------------
/docs/resources/deploy_images/google_origins.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/resources/deploy_images/google_origins.png
--------------------------------------------------------------------------------
/docs/resources/deploy_images/navbar_ec2_elasticIP.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/resources/deploy_images/navbar_ec2_elasticIP.png
--------------------------------------------------------------------------------
/docs/resources/hero.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/resources/hero.png
--------------------------------------------------------------------------------
/docs/setting_up.md:
--------------------------------------------------------------------------------
1 | # Setting up
2 |
3 | ## Creating your account and deleting the default admin account
4 |
5 | To connect for the first time you'll have to use the default admin account. The email is "admin@admin.com" and the password is "admin".
6 |
7 | In the menu on the left of your screen you'll see the "accounts management section". There you'll find a form to create your first admin account and another to delete the default one.
8 |
9 | ## Retrieving your AWS credentials
10 |
11 | Here is a video from Quincy showing how to get your Amazon credentials to allow your instance to send through the Amazon Simple Email Service.
12 | https://www.youtube.com/watch?v=_7U03GVD4a8
13 |
14 | ## Last step: Getting out of Amazon's sandbox mode
15 |
16 | Right now you can only send emails to the verified address you provided during the last step.
17 |
18 | Here is a link on how to proceed.
19 | https://docs.aws.amazon.com/ses/latest/DeveloperGuide/request-production-access.html
20 |
21 | When filling the form, be aware that they will want to know about how you plan to handle bounces and complaints. You will send your emails through their service and if you let the complaints pile up you will lower the reputation of their servers, degrading their service. Show them you understand that. Be serious and honest while filling your submission.
22 |
--------------------------------------------------------------------------------
/docs/settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/docs/settings.png
--------------------------------------------------------------------------------
/docs/static/javascripts/modal.js:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Modal"
3 | date: 2017-11-06T07:40:51-06:00
4 | draft: true
5 | ---
6 |
7 |
--------------------------------------------------------------------------------
/docs/static/stylesheets/modal.css:
--------------------------------------------------------------------------------
1 | --- title:"Modal" date:2017-11-06T07:41:14-06:00 --- @media(min-width:960px){#modal-wrap{display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.6);flex-direction:column;justify-content:center;align-items:center}#modal{display:flex;flex-direction:column;justify-content:center;margin:0 auto;width:100%}#modal-image{max-height:70vh;margin:5px auto}#modal-close-button{display:flex;flex-direction:row;justify-content:flex-end}#modal-close-button h2{color:#fff;cursor:pointer;font-size:50px;font-weight:bold}}
--------------------------------------------------------------------------------
/docs/stylesheets/misc.css:
--------------------------------------------------------------------------------
1 | .wrapper:first-of-type h1{
2 | padding-bottom: 10px;
3 | margin-top: -40px;
4 | font-size: 36px;
5 | }
6 |
7 | @media (max-width: 450px) {
8 | .wrapper:first-of-type h1{
9 | font-size: 30px;
10 | }
11 | }
--------------------------------------------------------------------------------
/docs/stylesheets/modal.css:
--------------------------------------------------------------------------------
1 | @media (min-width: 960px) {
2 | #modal-wrap {
3 | display: none;
4 | position: fixed;
5 | top: 0;
6 | left: 0;
7 | right: 0;
8 | bottom: 0;
9 | background: rgba(0, 0, 0, .6);
10 | flex-direction: column;
11 | justify-content: center;
12 | align-items: center;
13 | }
14 |
15 | #modal {
16 | display: flex;
17 | flex-direction: column;
18 | justify-content: center;
19 | margin: 0 auto;
20 | width: 100%;
21 | }
22 |
23 | #modal-image {
24 | max-height: 70vh;
25 | margin: 5px auto;
26 | }
27 | #modal-close-button {
28 | display: flex;
29 | flex-direction: row;
30 | justify-content: flex-end;
31 | }
32 |
33 | #modal-close-button p {
34 | color: #fff;
35 | cursor: pointer;
36 | font-size: 50px;
37 | font-weight: bold;
38 | margin-bottom: 20px;
39 | }
40 | }
--------------------------------------------------------------------------------
/docs/stylesheets/temporary.css:
--------------------------------------------------------------------------------
1 | /* This file only exists (temporarily) until the
2 | custom styling can be replaced with the
3 | implementation of the upstream project.
4 | */
5 |
6 | blockquote {
7 | padding: 0 20px;
8 | margin: 0 0 20px;
9 | font-size: inherit;
10 | border-left: 5px solid #eee;
11 | }
12 |
--------------------------------------------------------------------------------
/docs/tags/index.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Tags on Mail For Good
5 | https://freeCodeCamp.github.io/mail-for-good/tags/
6 | Recent content in Tags on Mail For Good
7 | Hugo -- gohugo.io
8 | en-us
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/public/assets/btn_google_signin_dark_normal_web@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/public/assets/btn_google_signin_dark_normal_web@2x.png
--------------------------------------------------------------------------------
/public/assets/btn_google_signin_light_disabled_web@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/public/assets/btn_google_signin_light_disabled_web@2x.png
--------------------------------------------------------------------------------
/public/assets/btn_google_signin_light_focus_web@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/public/assets/btn_google_signin_light_focus_web@2x.png
--------------------------------------------------------------------------------
/public/assets/btn_google_signin_light_normal_web@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/public/assets/btn_google_signin_light_normal_web@2x.png
--------------------------------------------------------------------------------
/public/assets/btn_google_signin_light_pressed_web@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freeCodeCamp/mail-for-good/dc946e244be12a9c238d246df364817e26373ffa/public/assets/btn_google_signin_light_pressed_web@2x.png
--------------------------------------------------------------------------------
/server/config/passport/local.js:
--------------------------------------------------------------------------------
1 | const LocalStrategy = require('passport-local').Strategy;
2 | const db = require('../../models');
3 |
4 | module.exports = (passport) => {
5 | passport.use('local-login',new LocalStrategy(strategyOptions ,authenticationProcess))
6 | };
7 |
8 | const strategyOptions = {
9 | usernameField:'email',
10 | passwordField:'password'
11 | }
12 |
13 | const authenticationProcess = (email,password,done) => {
14 | db.user.findOne({
15 | where: {
16 | email: email
17 | }
18 | }).then( user => {
19 | if(user === null){
20 | done('No account found',false)
21 | }else{
22 | db.user.checkPassword(password,user.password)
23 | .then((isValid) => {
24 | if(isValid === true){
25 | done(null,user)
26 | }else{
27 | done('Wrong email/password',false);
28 | }
29 | }).catch((error) => {
30 | if(user && user.password === null){
31 | done('An account exists but doesn\'t log via a password',false)
32 | }
33 | })
34 | }
35 | })
36 | }
37 |
--------------------------------------------------------------------------------
/server/config/secrets.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | sessionSecret: process.env.SESSION_SECRET || 'TestingTesting',
3 | mongo: process.env.MONGO_DB,
4 |
5 | email: {
6 | amazon: {
7 | accessKeyId: process.env.AMAZON_ACCESS_KEY_ID || null,
8 | secretAccessKey: process.env.AMAZON_SECRET_ACCESS_KEY || null
9 | }
10 | },
11 |
12 | smtpServer: {
13 | port: process.env.SMTP_TEST_PORT || '2025', // Linux envs disallow use of port <= 1024 without root
14 | host: process.env.SMTP_TEST_HOST || '127.0.0.1'
15 | },
16 |
17 | google: {
18 | consumerKey: process.env.GOOGLE_CONSUMER_KEY,
19 | consumerSecret: process.env.GOOGLE_CONSUMER_SECRET,
20 | callbackURL: process.env.GOOGLE_CALLBACK || 'http://localhost:8080/auth/google/callback'
21 | },
22 | };
23 |
--------------------------------------------------------------------------------
/server/config/sequelize_config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "development": {
3 | "username": process.env.PSQL_USERNAME,
4 | "password": process.env.PSQL_PASSWORD,
5 | "database": process.env.PSQL_DATABASE,
6 | "host": process.env.PSQL_HOST || "127.0.0.1",
7 | "dialect": "postgres",
8 | "logging": false,
9 | "url": process.env.DATABASE_URL,
10 | },
11 | "test": {
12 | "username": "postgres", // hacky fix for test envs
13 | "password": "",
14 | "database": "test",
15 | "host": process.env.PSQL_HOST || "127.0.0.1",
16 | "dialect": "postgres",
17 | "logging": false
18 | },
19 | "production": {
20 | "username": process.env.PSQL_USERNAME,
21 | "password": process.env.PSQL_PASSWORD,
22 | "database": process.env.PSQL_DATABASE,
23 | "host": process.env.PSQL_HOST || "127.0.0.1",
24 | "dialect": "postgres",
25 | "logging": false,
26 | "url": process.env.DATABASE_URL,
27 | "ssl": process.env.HEROKU ? true : false,
28 | "dialectOptions": process.env.HEROKU ? {"ssl":true} : {}
29 | }
30 | };
31 |
--------------------------------------------------------------------------------
/server/config/server/io.js:
--------------------------------------------------------------------------------
1 | const socketIo = require('socket.io');
2 |
3 | module.exports = (sessionMiddleware, server) => {
4 | // Websocket notifications
5 | const getProfile = require('../../controllers/websockets/get-profile');
6 |
7 | const io = socketIo(server);
8 | io.use(function(socket, next) {
9 | sessionMiddleware(socket.request, socket.request.res, next);
10 | });
11 | io.on('connection', socket => {
12 | if (!socket.request.session.passport) {
13 | return;
14 | }
15 | socket.request.session.passport.socket = socket.id;
16 | socket.request.session.save(() => {
17 |
18 | /**
19 | * Save the new socket id to the user's session stored in Redis
20 | * This ensures that on refresh, users still get websockets.
21 | */
22 |
23 | socket.on('login', () => {
24 | // Pass the user their user info for display on the frontend
25 | const id = socket.request.session.passport.user;
26 | getProfile(id).then(userObject => {
27 | socket.emit('loginResponse', userObject);
28 | });
29 | });
30 |
31 | });
32 | });
33 |
34 | return io;
35 | };
36 |
--------------------------------------------------------------------------------
/server/config/server/passport.js:
--------------------------------------------------------------------------------
1 | const passport = require('passport');
2 |
3 | const secrets = require('../secrets');
4 | const db = require('../../models');
5 | const Google = require('../passport/google');
6 | const local = require('../passport/local')
7 |
8 | module.exports = () => {
9 | passport.serializeUser(function(user, done) {
10 | done(null, user.id);
11 | });
12 |
13 | passport.deserializeUser(function(id, done) {
14 | db.user.findById(id).then(user => {
15 | done(null, user);
16 | return null;
17 | }).catch(err => {
18 | if (err) { throw err; }
19 | }
20 | );
21 | });
22 | ///////////////////////////////
23 | /* AUTHENTICATION STRATEGIES */
24 | ///////////////////////////////
25 | if(typeof process.env.GOOGLE_CONSUMER_KEY !== 'undefined'){
26 | Google(passport, secrets.google);
27 | }
28 | local(passport)
29 | };
30 |
--------------------------------------------------------------------------------
/server/config/server/redis.js:
--------------------------------------------------------------------------------
1 | const redis = require('redis');
2 |
3 | module.exports = () => {
4 | const redisSettings = {
5 | host: process.env.REDIS_HOST || '127.0.0.1',
6 | port: process.env.REDIS_PORT,
7 | password: process.env.REDIS_PASSWORD,
8 | };
9 | const redisUrl = process.env.REDIS_URL;
10 |
11 | const client = createRedisClient(redisSettings,redisUrl);
12 | const subscriber = createRedisClient(redisSettings,redisUrl); // Need to create separate connections
13 | const publisher = createRedisClient(redisSettings,redisUrl); // for pub-sub
14 |
15 | client.on('error', err => console.log('Error: Are you running redis? - ', err));
16 |
17 | return {
18 | client,
19 | subscriber,
20 | publisher,
21 | };
22 | };
23 |
24 |
25 | function createRedisClient(settings, url) {
26 | let client;
27 |
28 | if(url){
29 | client = redis.createClient(url);
30 | }else{
31 | client = redis.createClient(settings);
32 | }
33 |
34 | return client;
35 | }
36 |
--------------------------------------------------------------------------------
/server/config/server/restore-db-state.js:
--------------------------------------------------------------------------------
1 | const Campaign = require('../../models').campaign;
2 | const User = require('../../models').user;
3 |
4 |
5 | /**
6 | * Update statuses to correct the database state after shutdown/crash of the app.
7 | * Example: if a campaign is sending (status: 'sending') when the app stops,
8 | * the status will still be 'sending' even though sending has been interrupted.
9 | * In this case, update the status to 'interrupted',
10 | * allowing the user to resume sending a campaign manually.
11 | */
12 | module.exports = () => {
13 | // If there are no users then the database is
14 | // probably fresh + there is no need to
15 | // update anything
16 | return User.findAll({ raw: true })
17 | .then(users => {
18 | if (users.length) {
19 | Campaign.update({
20 | status: 'interrupted'
21 | }, {
22 | where: { status: 'sending' }
23 | });
24 | }
25 | return null;
26 | })
27 | // If the promise is rejected, the table doesn't exist. This is fine, as it may be the first
28 | // time the app was run. We can discard the error.
29 | .catch(() => {});
30 | };
31 |
--------------------------------------------------------------------------------
/server/config/server/sequelize.js:
--------------------------------------------------------------------------------
1 | const db = require('../../models');
2 |
3 | module.exports = () => {
4 | const { sequelize } = db;
5 |
6 | sequelize.sync({force:false,hooks:true})
7 | };
8 |
--------------------------------------------------------------------------------
/server/config/server/session.js:
--------------------------------------------------------------------------------
1 | const session = require('express-session');
2 | const RedisStore = require('connect-redis')(session);
3 |
4 | const secret = require('../secrets');
5 |
6 | module.exports = client => {
7 | // Session middleware
8 | const sessionMiddleware = session({
9 | store: new RedisStore({ client }),
10 | secret: secret.sessionSecret,
11 | resave: false,
12 | saveUninitialized: false
13 | });
14 |
15 | return { sessionMiddleware };
16 | };
17 |
--------------------------------------------------------------------------------
/server/config/server/webpack-dev-middleware.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const history = require('connect-history-api-fallback');
3 |
4 | /*
5 | With thanks to https://github.com/coryhouse/react-slingshot
6 | This app was kickstarted with this generator.
7 | */
8 |
9 | module.exports = app => {
10 | if (process.env.NODE_ENV === 'development') {
11 | const webpackDevMiddleware = require('webpack-dev-middleware');
12 | const webpackHotMiddleware = require('webpack-hot-middleware');
13 |
14 | const devWebpackConfig = require('../../../webpack.config.dev');
15 | const compiler = webpack(devWebpackConfig);
16 |
17 | app.use(history());
18 | app.use(webpackDevMiddleware(compiler, {
19 | publicPath: devWebpackConfig.output.publicPath,
20 | noInfo: false,
21 | quiet: false,
22 | stats: {
23 | assets: false,
24 | colors: true,
25 | version: false,
26 | hash: false,
27 | timings: false,
28 | chunks: false,
29 | chunkModules: false
30 | },
31 | }));
32 | app.use(webpackHotMiddleware(compiler));
33 | }
34 | };
35 |
--------------------------------------------------------------------------------
/server/controllers/accountsManagement/create-user.js:
--------------------------------------------------------------------------------
1 | const db = require('../../models');
2 |
3 | module.exports = function(req, res) {
4 | Promise.all([
5 | db.user.getIsAdmin(req.body.data.queriersEmail),
6 | db.user.checkIfUserExists(req.body.data.email)
7 | ]).then((checks) => {
8 | if(checks[0] === false){
9 | res.status(403).send('You don\'t have the rights to create a user.')
10 | }else if(checks[1] === true){
11 | res.status(409).send('This user already exists.')
12 | }else{
13 | db.user.createOne(req.body.data)
14 | .catch((error,errorStatus) => {res.status(500).send('The application has\'nt been able to create the user.')})
15 | .then(success => {res.status(200).send('User successfully created.')})
16 | }
17 | return null
18 | }).catch(error => {
19 | console.log(error);
20 | res.status(500).send(error.message);
21 | })
22 | }
23 |
--------------------------------------------------------------------------------
/server/controllers/accountsManagement/delete-user.js:
--------------------------------------------------------------------------------
1 | const db = require('../../models');
2 |
3 |
4 | module.exports = function(req, res) {
5 | Promise.all([
6 | db.user.getIsAdmin(req.body.queriersEmail),
7 | db.user.checkIfUserExists(req.body.email)
8 | ]).then((checks) => {
9 | if(checks[0] === false){
10 | res.status(403).send('You don\'t have the rights to delete this user.')
11 | }else if(checks[1] === false){
12 | res.status(409).send('The account you are trying to delete doesn\t exist.')
13 | }else{
14 | db.user.destroy({where:{email:req.body.email}})
15 | .catch((error,errorStatus) => {res.status(500).send('The application has\'nt been able to delete the user.')})
16 | .then(success => {res.status(200).send('User successfully deleted.')})
17 | }
18 | return null
19 | }).catch(error => {
20 | console.log(error);
21 | res.status(500).send(error.message);
22 | })
23 | }
24 |
--------------------------------------------------------------------------------
/server/controllers/analytics/get-clickthroughs.js:
--------------------------------------------------------------------------------
1 | const Campaign = require('../../models').campaign;
2 | const CampaignAnalytics = require('../../models').campaignanalytics;
3 | const CampaignAnalyticsLink = require('../../models').campaignanalyticslink;
4 |
5 | // Temporary route for getting clickthrough data
6 | // Will later integrate this into subscriber/campaign/list routes
7 | // ~ at the moment it only gets CTR per campaign
8 | module.exports = function(req, res) {
9 | const campaignId = req.query.campaignId;
10 | const userId = req.user.id;
11 |
12 | Campaign.findOne({
13 | where: {id: campaignId, userId},
14 | include: [{model: CampaignAnalytics, include: [{model: CampaignAnalyticsLink}]}]
15 | }).then(result => {
16 | console.log(result);
17 | if (result) {
18 | res.send(result.campaignanalytic.campaignanalyticslinks);
19 | } else {
20 | res.status(400).send('you do not have permissions or this campaign does not exist')
21 | }
22 | })
23 | }
24 |
--------------------------------------------------------------------------------
/server/controllers/campaign/delete-campaigns.js:
--------------------------------------------------------------------------------
1 | const campaign = require('../../models').campaign;
2 |
3 | module.exports = (req, res) => {
4 | const campaignIds = req.body.data;
5 |
6 | const userId = req.user.id;
7 |
8 | campaign.destroy({
9 | where: {
10 | id: { in: campaignIds
11 | },
12 | userId
13 | }
14 | }).then(numDeleted => {
15 | if (numDeleted) {
16 | res.send();
17 | } else {
18 | res.status(404).send();
19 | }
20 | }).catch(err => {
21 | throw err;
22 | });
23 | };
24 |
--------------------------------------------------------------------------------
/server/controllers/campaign/email/amazon-ses/config/configSes.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @description Converts {{variables}} in the email body to their equivalent in the db
3 | * @param {object} task - The email to create
4 | * @param {object} campaignInfo - Information about this campaign
5 | * @return {string} HTML with {{variables}} replaced by what they represent. E.g., {{cat}} may become 'Garfield'
6 | */
7 |
8 | const AWS = require('aws-sdk');
9 |
10 | module.exports = (accessKey, secretKey, region) => {
11 | const isProductionTestMode = process.env.TEST_PRODUCTION === 'true';
12 | const isDevMode = process.env.NODE_ENV === 'development' || isProductionTestMode;
13 |
14 | const ses = (isDevMode || isProductionTestMode)
15 | ? new AWS.SES({ // Dev mode
16 | apiVersion: '2010-12-01',
17 | // convertResponseTypes: false,
18 | accessKeyId: accessKey,
19 | secretAccessKey:
20 | secretKey,
21 | region,
22 | endpoint: 'http://localhost:9999'
23 | })
24 | : new AWS.SES({ // Prod mode
25 | apiVersion: '2010-12-01',
26 | accessKeyId: accessKey,
27 | secretAccessKey: secretKey,
28 | region
29 | });
30 |
31 | return ses;
32 | };
33 |
--------------------------------------------------------------------------------
/server/controllers/campaign/email/amazon-ses/controllers/finishCampaignSend.js:
--------------------------------------------------------------------------------
1 | const db = require('../../../../../models');
2 |
3 | /**
4 | * @description Change the campaign status on finishing a campaign send
5 | * @param {boolean} cancelCampaignSend - Flag for whether or not this campaign was cancelled
6 | * @param {object} campaignInfo - Information about this campaign
7 | */
8 |
9 | module.exports = (cancelCampaignSend, campaignInfo) => {
10 | const status = cancelCampaignSend ? 'interrupted' : 'done';
11 | db.campaign.update({
12 | status
13 | }, {
14 | where: {
15 | id: campaignInfo.campaignId
16 | }
17 | });
18 | };
19 |
--------------------------------------------------------------------------------
/server/controllers/campaign/email/amazon-ses/controllers/getArrayOfEmailIds.js:
--------------------------------------------------------------------------------
1 | const db = require('../../../../../models');
2 |
3 | /**
4 | * @description Get all the 'listsubscriber' ids we will email
5 | * @param {object} campaignInfo - Information about this campaign
6 | * @return {array} Plain array of unique listsubscriber ids {number}, we will email these users.
7 | */
8 |
9 | module.exports = (campaignInfo) => {
10 | function getListSubscriberIds() {
11 | return db.listsubscriber.findAll({
12 | where: {
13 | listId: campaignInfo.listId,
14 | subscribed: true
15 | },
16 | include: [{
17 | model: db.campaignsubscriber,
18 | where: {
19 | campaignId: campaignInfo.campaignId,
20 | sent: false
21 | }
22 | }],
23 | attributes: [
24 | 'id'
25 | ],
26 | raw: true
27 | })
28 | .then(instances => {
29 | const plainArrayOfIdNumbers = instances.map(x => x.id);
30 | return plainArrayOfIdNumbers;
31 | })
32 | .catch(err => {
33 | // This should never happen - so we can throw an error here if it does.
34 | throw new Error('Error getting list subscribers ids in getArrayOfEmailIds - ', err);
35 | });
36 | }
37 |
38 | return getListSubscriberIds();
39 | };
40 |
--------------------------------------------------------------------------------
/server/controllers/campaign/email/amazon-ses/controllers/updateCampaignStatus.js:
--------------------------------------------------------------------------------
1 | const db = require('../../../../../models');
2 |
3 | /**
4 | * @description Update the campaign status to 'sending'
5 | * @param {object} campaignInfo - Information about this campaign
6 | */
7 |
8 | module.exports = (campaignInfo) => {
9 | function updateCampaignStatus() {
10 | db.campaign.update({
11 | status: 'sending'
12 | }, {
13 | where: {
14 | id: campaignInfo.campaignId
15 | }
16 | }).catch(err => {throw err;});
17 | }
18 |
19 | return updateCampaignStatus();
20 | };
21 |
--------------------------------------------------------------------------------
/server/controllers/campaign/email/amazon-ses/lib/amazon.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @description Create an Amazon email based on the SES spec
3 | * @param {object} task - The email to create
4 | * @param {object} campaignInfo - Information about this campaign
5 | * @return {object} Formatted email object
6 | */
7 |
8 |
9 | module.exports = (task, campaignInfo) => {
10 |
11 | // Ref https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/SES.html#sendEmail-property
12 | const email = {
13 | Source: `"${campaignInfo.fromName}" <${campaignInfo.fromEmail}>`, // From email
14 | Destination: { // To email
15 | ToAddresses: [`<${task.email}>`] // Set name as follows https://docs.aws.amazon.com/ses/latest/DeveloperGuide/email-format.html
16 | },
17 | Message: {
18 | Body: {},
19 | Subject: { // Subject
20 | Data: campaignInfo.emailSubject
21 | }
22 | }
23 | };
24 |
25 | if (campaignInfo.type === 'Plaintext') { // Send as plaintext if plaintext, else send as HTML (no other format concerns us)
26 | Object.assign(email.Message.Body, { Text: { Data: campaignInfo.emailBody } });
27 | } else {
28 | Object.assign(email.Message.Body, { Html: { Data: campaignInfo.emailBody } });
29 | }
30 |
31 | return { email, task };
32 | };
33 |
--------------------------------------------------------------------------------
/server/controllers/campaign/email/amazon-ses/lib/mail-merge.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @description Converts {{variables}} in the email body to their equivalent in the db
3 | * @param {object} task - The email to create
4 | * @param {object} campaignInfo - Information about this campaign
5 | * @return {string} HTML with {{variables}} replaced by what they represent. E.g., {{cat}} may become 'Garfield'
6 | */
7 |
8 | const Handlebars = require('handlebars');
9 |
10 | module.exports = (task, campaignInfo) => {
11 | // can pre-compile this to save time
12 | const bodyTemplate = Handlebars.compile(campaignInfo.emailBody);
13 |
14 | let data = task.additionalData;
15 | data.email = task.email;
16 |
17 | return bodyTemplate(data);
18 | };
19 |
--------------------------------------------------------------------------------
/server/controllers/campaign/email/amazon-ses/lib/nest-array.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @description Converts an array into a nested array where nested array are of the {length} parameter
3 | * @param {number} length - The length of each nested array
4 | * @param {array} array - The array to act on
5 | * @return {array} Nested array
6 | * @example nestArray(2, [1,2,3,4,5,6,7]) returns [[1,2], [3, 4], [5, 6], [7]]
7 | */
8 |
9 | module.exports = (length, array) => {
10 | if (length <= 0) {
11 | return array;
12 | }
13 | if (length > array.length) {
14 | return [array];
15 | }
16 |
17 | let tempArray = [];
18 | const newArray = [];
19 |
20 | array.forEach((item, index) => {
21 | tempArray.push(item);
22 | if ((index + 1) % length === 0) {
23 | newArray.push(tempArray);
24 | tempArray = [];
25 | }
26 | else if (index === array.length - 1) {
27 | newArray.push(tempArray);
28 | }
29 | });
30 |
31 | return newArray;
32 | };
33 |
--------------------------------------------------------------------------------
/server/controllers/campaign/email/amazon-ses/notifications/sendFinalNotification.js:
--------------------------------------------------------------------------------
1 | const sendSingleNotification = require('../../../../websockets/send-single-notification');
2 |
3 | /**
4 | * @description Send a final notification to the user informing them of the campaign's success
5 | * @param {string} outcome - The outcome of the email send
6 | * @param {object} campaignInfo - Information about this campaign
7 | * @param {object} io - Configured Redis instance
8 | * @param {object} req - The express request
9 | */
10 |
11 | module.exports = (outcome, campaignInfo, io, req) => {
12 | if (outcome == 'success') {
13 | const notification = {
14 | message: `Campaign "${campaignInfo.name}" has been sent`,
15 | icon: 'fa-envelope',
16 | iconColour: 'text-green',
17 | };
18 |
19 | sendSingleNotification(io, req, notification);
20 | }
21 | };
22 |
--------------------------------------------------------------------------------
/server/controllers/campaign/email/amazon-ses/notifications/sendUpdateEmailsSentNotification.js:
--------------------------------------------------------------------------------
1 | const db = require('../../../../../models');
2 | const sendUpdateNotification = require('../../../../websockets/send-update-notification');
3 |
4 | /**
5 | * @description Send an update notification letting the user know how many emails have been sent
6 | * @param {object} campaignInfo - Information about this campaign
7 | * @param {object} io - Configured Redis instance
8 | * @param {object} req - The express request
9 | * @return {function} a function to call to send the update notification.
10 | */
11 |
12 | module.exports = (campaignInfo, io, req) => {
13 | const id = (Math.random() * 100000).toString();
14 |
15 | const notification = () => {
16 | db.campaignanalytics.findOne({
17 | where: { campaignId: campaignInfo.campaignId },
18 | attributes: ['totalSentCount']
19 | }).then(campaignAnalytics => {
20 | const notification = {
21 | message: `Sent ${campaignAnalytics.totalSentCount} emails`,
22 | id, // Unique identified for use on client side (in the reducer)
23 | icon: 'fa-envelope',
24 | iconColour: 'text-green'
25 | };
26 |
27 | sendUpdateNotification(io, req, notification);
28 | });
29 | };
30 |
31 | return notification;
32 | };
33 |
--------------------------------------------------------------------------------
/server/controllers/campaign/email/amazon-ses/utils/sendCampaignSuccessEmail.js:
--------------------------------------------------------------------------------
1 | const AmazonEmail = require('../lib/amazon');
2 |
3 | /**
4 | * @description Sends an email to the owner of the campaign informing them of the success of the campaign.
5 | * @param {object} campaignInfo - Information about this campaign
6 | * @param {Date} startTime - The Date that this campaign began sending
7 | * @param {object} ses - Configured Amazon SES SDK instance
8 | */
9 |
10 |
11 | module.exports = (campaignInfo, startTime, ses) => {
12 | const data = {
13 | durationMs: new Date() - startTime,
14 | campaignName: campaignInfo.name,
15 | fromEmail: campaignInfo.fromEmail
16 | };
17 |
18 | const fromName = `Mail for Good`;
19 | const emailSubject = `MfG: campaign delivered`;
20 | const emailBody = `Campaign: "${data.campaignName}" has been successfully delivered.
21 |
22 | Duration: ${(data.durationMs/1000/60).toFixed(2)} minutes`;
23 |
24 | ses.sendEmail(AmazonEmail(
25 | { email: data.fromEmail },
26 | { fromEmail: data.fromEmail, fromName, emailBody, emailSubject, type: 'Plaintext' }
27 | ).email, (data, err) => {
28 | console.log("Sent campaign delivery success email")
29 | console.log(err);
30 | });
31 | };
32 |
--------------------------------------------------------------------------------
/server/controllers/campaign/email/index.js:
--------------------------------------------------------------------------------
1 | const amazonController = require('./amazon-ses');
2 |
3 | module.exports = {
4 | amazon: {
5 | controller: amazonController,
6 | }
7 | };
8 |
--------------------------------------------------------------------------------
/server/controllers/campaign/send-campaign.spec.js:
--------------------------------------------------------------------------------
1 | const { expect } = require('chai');
2 | const rewire = require('rewire');
3 |
4 | const sendCampaign = rewire('./send-campaign');
5 |
6 | describe('sendCampaign', () => {
7 | describe('howLongEmailingWillTake', () => {
8 | const howLongEmailingWillTake = sendCampaign.__get__('howLongEmailingWillTake');
9 |
10 | it('calculates how long email sending will take', () => {
11 | });
12 |
13 | it('calculates the number of remaining emails', () => {
14 | });
15 |
16 | it('shows how many subscribers the campaign will be sent to', () => {
17 | let expectedMessage = 'Your campaign is being sent to 10 subscribers, it should be done in a few seconds. Your Amazon limit for today is now 190 emails.';
18 | expect(howLongEmailingWillTake(10, 200, 100, 'ready').message).to.be.equal(expectedMessage);
19 |
20 | expectedMessage = 'Your campaign is being sent to 1,000 subscribers, it should be done in a few seconds. Your Amazon limit for today is now 199,000 emails.';
21 | expect(howLongEmailingWillTake(1000, 200000, 100, 'ready').message).to.be.equal(expectedMessage);
22 | });
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/server/controllers/campaign/stop-campaign-sending.js:
--------------------------------------------------------------------------------
1 | const {
2 | // sequelize,
3 | campaign: Campaign
4 | } = require('../../models');
5 |
6 |
7 | module.exports = (req, res, redis) => {
8 |
9 | // If req.body.id was not supplied or is not a number, cancel
10 | if (!req.body.id || typeof req.body.id !== 'number') {
11 | res.status(400).send();
12 | return;
13 | }
14 | const userId = req.user.id;
15 | const campaignId = req.body.id;
16 |
17 | Campaign.findOne({
18 | where: {
19 | userId,
20 | id: campaignId
21 | },
22 | raw: true
23 | }).then(campaign => {
24 | if (campaign) {
25 | redis.publisher.publish('stop-campaign-sending', campaignId);
26 |
27 | Campaign.update({ status: 'interrupted' }, {
28 | where: { userId, id: campaignId }
29 | }).then(() => {
30 | res.send();
31 | });
32 | } else {
33 | res.status(400).send();
34 | }
35 | });
36 | };
37 |
--------------------------------------------------------------------------------
/server/controllers/list/add-subscribers.js:
--------------------------------------------------------------------------------
1 | const SubscriberModel = require('../../models').subscriber;
2 |
3 | module.exports = function(req, res) {
4 | const subscribers = req.body.subscribers;
5 | // const fields = req.body.fields;
6 |
7 | subscribers.forEach(subscriber => {
8 | SubscriberModel.findOne({
9 | where: {
10 | email: subscriber.email
11 | }
12 | }).then(email => {
13 | if (email) {
14 | res.status(400).send({status: 'error', message: 'This email already exists'});
15 | } else {
16 | SubscriberModel.create({email: subscriber.email}).then(() => {
17 | res.status(201).send({status: 'success', message: `${subscriber.email} added succesfully`});
18 | });
19 | }
20 | });
21 | });
22 | };
23 |
--------------------------------------------------------------------------------
/server/controllers/list/delete-lists.js:
--------------------------------------------------------------------------------
1 | const List = require('../../models').list;
2 | const ListSubscriber = require('../../models').listsubscriber;
3 |
4 | module.exports = (req, res) => {
5 |
6 | const userId = req.user.id;
7 | const listIds = req.body.lists;
8 |
9 | List.destroy({
10 | where: { userId, id: listIds }
11 | })
12 | .then(() => {
13 | res.send('Lists deleted');
14 | // Deleting list subscribers may take some time.
15 | // Therefore we can respond when we know that the lists were deleted.
16 | // ListSubscribers will only be deleted when lists have themselves been destroyed.
17 | // Lists will only be destroyed if each listId has an id matching the userId
18 | return ListSubscriber.destroy({
19 | where: { listId: listIds }
20 | });
21 | })
22 | .catch(() => {
23 | res.status(400).send('Failed to delete lists');
24 | });
25 | };
26 |
--------------------------------------------------------------------------------
/server/controllers/list/delete-subscribers.js:
--------------------------------------------------------------------------------
1 | const List = require('../../models').list;
2 | const ListSubscriber = require('../../models').listsubscriber;
3 |
4 | module.exports = (req, res) => {
5 |
6 | const listSubscriberIds = req.body.listSubscribers;
7 | const userId = req.user.id;
8 |
9 | List.findAll({
10 | where: { userId },
11 | attributes: [ 'id' ]
12 | }).then(result => {
13 | const ownedListIds = result.map(list => {
14 | return list.id;
15 | });
16 |
17 | ListSubscriber.destroy({
18 | where: {
19 | id: listSubscriberIds,
20 | listId: ownedListIds
21 | }
22 | }).then(numDeleted => {
23 | if (numDeleted) {
24 | res.send('Subscribers deleted');
25 | } else {
26 | res.status(404).send();
27 | }
28 | });
29 | }).catch(() => {
30 | res.status(500).send('Failed to delete subscribers');
31 | });
32 | };
33 |
--------------------------------------------------------------------------------
/server/controllers/list/get-lists.js:
--------------------------------------------------------------------------------
1 | const list = require('../../models').list;
2 |
3 | module.exports = (req, res) => {
4 | // Find all lists belonging to a user & send it to them
5 | let userId = req.user.id;
6 |
7 | list.findAll({
8 | where: {
9 | userId
10 | },
11 | attributes: [
12 | 'name',
13 | 'subscribeKey',
14 | 'createdAt',
15 | 'updatedAt',
16 | 'id',
17 | 'additionalFields',
18 | 'status',
19 | 'total'
20 | ],
21 | raw: true
22 | }).then(instancesArray => {
23 | if (instancesArray) {
24 | res.send(instancesArray);
25 | } else {
26 | res.send();
27 | }
28 | }).catch(() => res.send());
29 | };
30 |
--------------------------------------------------------------------------------
/server/controllers/list/update-list.js:
--------------------------------------------------------------------------------
1 | const List = require('../../models').list;
2 |
3 | module.exports = (req, res) => {
4 |
5 | const listId = req.body.id;
6 | const values = req.body.values;
7 |
8 | List.update(
9 | values,
10 | {where:{id:listId}}
11 | )
12 | .then(()=>{
13 | res.send('Name updated');
14 | })
15 | .catch(()=>{
16 | res.status(400).send('Failed to update list');
17 | });
18 | };
19 |
--------------------------------------------------------------------------------
/server/controllers/permissions/acl-lib/acl-campaign-permissions.js:
--------------------------------------------------------------------------------
1 | const ACL = require('../../../models').acl;
2 |
3 | module.exports = function(id, userId) {
4 |
5 | /*
6 | Returns a promise containing the type of permission granted
7 | @params id = primaryKey id to a row in the ACL table. Extracted from req.cookies.user (if provided)
8 | @params userId = the logged in user's id
9 | */
10 |
11 | // If id is undefined, no cookie has been set. Therefore the user is not accessing another's account and has full access be default.
12 | if (!id) {
13 | return Promise.resolve({
14 | userId,
15 | access: 'Write'
16 | });
17 | }
18 |
19 | return ACL.findById(id)
20 | .then(userInstance => {
21 | if (userInstance) {
22 | const campaignAccess = {
23 | access: userInstance.getDataValue('campaigns'),
24 | userId: userInstance.getDataValue('userId')
25 | };
26 | return campaignAccess;
27 | } else {
28 | throw 'Permission denied';
29 | }
30 | })
31 | .catch(err => {
32 | throw err;
33 | });
34 | };
35 |
--------------------------------------------------------------------------------
/server/controllers/permissions/acl-lib/acl-list-permissions.js:
--------------------------------------------------------------------------------
1 | const ACL = require('../../../models').acl;
2 |
3 | module.exports = function(id, userId) {
4 |
5 | /*
6 | Returns a promise containing the type of permission granted
7 | @params id = primaryKey id to a row in the ACL table. Extracted from req.cookies.user (if provided)
8 | @params userId = the logged in user's id
9 | */
10 |
11 | // If id is undefined, no cookie has been set. Therefore the user is not accessing another's account and has full access be default.
12 | if (!id) {
13 | return Promise.resolve({
14 | userId,
15 | access: 'Write'
16 | });
17 | }
18 |
19 | return ACL.findById(id)
20 | .then(userInstance => {
21 | if (userInstance) {
22 | const listsAccess = {
23 | access: userInstance.getDataValue('lists'),
24 | userId: userInstance.getDataValue('userId')
25 | };
26 | return listsAccess;
27 | } else {
28 | throw 'Permission denied';
29 | }
30 | })
31 | .catch(err => {
32 | throw err;
33 | });
34 | };
35 |
--------------------------------------------------------------------------------
/server/controllers/permissions/acl-lib/acl-template-permissions.js:
--------------------------------------------------------------------------------
1 | const ACL = require('../../../models').acl;
2 |
3 | module.exports = function(id, userId) {
4 |
5 | /*
6 | Returns a promise containing the type of permission granted
7 | @params id = primaryKey id to a row in the ACL table. Extracted from req.cookies.user (if provided)
8 | @params userId = the logged in user's id
9 | */
10 |
11 | // If id is undefined, no cookie has been set. Therefore the user is not accessing another's account and has full access be default.
12 | if (!id) {
13 | return Promise.resolve({
14 | userId,
15 | access: 'Write'
16 | });
17 | }
18 |
19 | return ACL.findById(id)
20 | .then(userInstance => {
21 | if (userInstance) {
22 | const templatesAccess = {
23 | access: userInstance.getDataValue('templates'),
24 | userId: userInstance.getDataValue('userId')
25 | };
26 | return templatesAccess;
27 | } else {
28 | throw 'Permission denied';
29 | }
30 | })
31 | .catch(err => {
32 | throw err;
33 | });
34 | };
35 |
--------------------------------------------------------------------------------
/server/controllers/permissions/delete-active-permissions.js:
--------------------------------------------------------------------------------
1 | const ACL = require('../../models').acl;
2 |
3 | module.exports = (req, res) => {
4 |
5 | let { offerIds } = req.body; // List of ids in acl to delete
6 | offerIds = typeof offerIds === 'object' ? offerIds : [offerIds];
7 | offerIds = offerIds.map(x => Number(x));
8 |
9 | ACL.destroy({
10 | where: {
11 | id: { in: offerIds },
12 | toUserId: String(req.user.id)
13 | }
14 | })
15 | .then(() => {
16 | res.send({ message: 'You no longer have access to these permissions' });
17 | })
18 | .catch(err => {
19 | throw err;
20 | });
21 | };
22 |
--------------------------------------------------------------------------------
/server/controllers/permissions/delete-grant-offered-permissions.js:
--------------------------------------------------------------------------------
1 | const OfferPermission = require('../../models').offerPermission;
2 |
3 | module.exports = (req, res) => {
4 |
5 | let { offerIds } = req.body; // List of ids in offerPermission to reject
6 | offerIds = typeof offerIds === 'object' ? offerIds : [offerIds];
7 | offerIds = offerIds.map(x => Number(x));
8 |
9 | OfferPermission.destroy({
10 | where: {
11 | id: { in: offerIds },
12 | userId: req.user.id
13 | }
14 | })
15 | .then(() => {
16 | res.send({ message: 'You have deleted these permission offers' });
17 | })
18 | .catch(err => {
19 | throw err;
20 | });
21 | };
22 |
--------------------------------------------------------------------------------
/server/controllers/permissions/delete-granted-permissions.js:
--------------------------------------------------------------------------------
1 | const ACL = require('../../models').acl;
2 |
3 | module.exports = (req, res) => {
4 |
5 | let { offerIds } = req.body; // List of ids in acl to delete
6 | offerIds = typeof offerIds === 'object' ? offerIds : [offerIds];
7 | offerIds = offerIds.map(x => Number(x));
8 |
9 | ACL.destroy({
10 | where: {
11 | id: { in: offerIds },
12 | userId: String(req.user.id)
13 | }
14 | })
15 | .then(() => {
16 | res.send({ message: 'You are no longer granting this user these permissions' });
17 | })
18 | .catch(err => {
19 | throw err;
20 | });
21 | };
22 |
--------------------------------------------------------------------------------
/server/controllers/permissions/get-active-permissions.js:
--------------------------------------------------------------------------------
1 | const ACL = require('../../models').acl;
2 |
3 | module.exports = function(req, res) {
4 |
5 | ACL.findAll({
6 | where: {
7 | toUserId: String(req.user.id)
8 | },
9 | attributes: [
10 | 'id',
11 | 'fromUserEmail',
12 | 'campaigns',
13 | 'templates',
14 | 'lists',
15 | 'createdAt',
16 | 'updatedAt'
17 | ],
18 | raw: true
19 | })
20 | .then(permissionArray => {
21 | if (!permissionArray.length) {
22 | res.send();
23 | } else {
24 | res.send(permissionArray);
25 | }
26 | })
27 | .catch(err => {
28 | throw err;
29 | });
30 | };
31 |
--------------------------------------------------------------------------------
/server/controllers/permissions/get-grant-offered-permissions.js:
--------------------------------------------------------------------------------
1 | const OfferPermission = require('../../models').offerPermission;
2 |
3 | module.exports = function(req, res) {
4 |
5 | OfferPermission.findAll({
6 | where: {
7 | userId: req.user.id
8 | },
9 | attributes: [
10 | 'id',
11 | 'toUserEmail',
12 | 'campaigns',
13 | 'templates',
14 | 'lists',
15 | 'createdAt',
16 | 'updatedAt'
17 | ],
18 | raw: true
19 | })
20 | .then(grantOfferedPermissionsArray => {
21 | if (!grantOfferedPermissionsArray.length) {
22 | res.send();
23 | } else {
24 | res.send(grantOfferedPermissionsArray);
25 | }
26 | })
27 | .catch(err => {
28 | throw err;
29 | });
30 | };
31 |
--------------------------------------------------------------------------------
/server/controllers/permissions/get-granted-permissions.js:
--------------------------------------------------------------------------------
1 | const ACL = require('../../models').acl;
2 |
3 | module.exports = function(req, res) {
4 |
5 | ACL.findAll({
6 | where: {
7 | userId: String(req.user.id)
8 | },
9 | attributes: [
10 | 'id',
11 | 'toUserEmail',
12 | 'campaigns',
13 | 'templates',
14 | 'lists',
15 | 'createdAt',
16 | 'updatedAt'
17 | ],
18 | raw: true
19 | })
20 | .then(permissionArray => {
21 | if (!permissionArray.length) {
22 | res.send();
23 | } else {
24 | res.send(permissionArray);
25 | }
26 | })
27 | .catch(err => {
28 | throw err;
29 | });
30 | };
31 |
--------------------------------------------------------------------------------
/server/controllers/permissions/get-received-permission-offers.js:
--------------------------------------------------------------------------------
1 | const OfferPermission = require('../../models').offerPermission;
2 |
3 | module.exports = function(req, res) {
4 |
5 | OfferPermission.findAll({
6 | where: {
7 | toUserId: String(req.user.id)
8 | },
9 | attributes: [
10 | 'id',
11 | 'fromUserEmail',
12 | 'campaigns',
13 | 'templates',
14 | 'lists',
15 | 'createdAt',
16 | 'updatedAt'
17 | ],
18 | raw: true
19 | })
20 | .then(receivedPermissionArray => {
21 | if (!receivedPermissionArray.length) {
22 | res.send();
23 | } else {
24 | res.send(receivedPermissionArray);
25 | }
26 | })
27 | .catch(err => {
28 | throw err;
29 | });
30 | };
31 |
--------------------------------------------------------------------------------
/server/controllers/permissions/reject-permission-offers.js:
--------------------------------------------------------------------------------
1 | const OfferPermission = require('../../models').offerPermission;
2 |
3 | module.exports = (req, res) => {
4 |
5 | let { offerIds } = req.body; // List of ids in offerPermission to reject
6 | offerIds = typeof offerIds === 'object' ? offerIds : [offerIds];
7 | offerIds = offerIds.map(x => Number(x));
8 |
9 | OfferPermission.destroy({
10 | where: {
11 | id: { in: offerIds },
12 | toUserId: String(req.user.id)
13 | }
14 | })
15 | .then(() => {
16 | res.send({ message: 'You have rejected these permission offers' });
17 | })
18 | .catch(err => {
19 | throw err;
20 | });
21 | };
22 |
--------------------------------------------------------------------------------
/server/controllers/settings/get-settings.js:
--------------------------------------------------------------------------------
1 | const Setting = require('../../models').setting;
2 |
3 | module.exports = function(req, res) {
4 |
5 | /*
6 | NOTE: This file returns boolean values to the client for fields that have values assigned.
7 | */
8 |
9 | Setting.findOne({
10 | where:{
11 | userId: req.user.id
12 | },
13 | attributes: [
14 | 'amazonSimpleEmailServiceAccessKey',
15 | 'amazonSimpleEmailServiceSecretKey',
16 | 'amazonSimpleQueueServiceUrl',
17 | 'region',
18 | 'whiteLabelUrl',
19 | 'email'
20 | ]
21 | }).then(settingsInstance => {
22 | const settingsObject = settingsInstance.get({ plain:true });
23 | const settingsObjectToBool = {};
24 | Object.keys(settingsObject).forEach(key => {
25 | if (settingsObject[key])
26 | settingsObjectToBool[key] = true;
27 | });
28 | res.send(settingsObjectToBool);
29 | }).catch(err => res.status(500).send(err));
30 |
31 | };
32 |
--------------------------------------------------------------------------------
/server/controllers/subscriber/unsubscribe.js:
--------------------------------------------------------------------------------
1 | const listsubscriber = require('../../models').listsubscriber;
2 |
3 | module.exports = (req, res) => {
4 | const unsubscribeKey = req.params.unsubscribeKey;
5 |
6 | listsubscriber.update(
7 | { subscribed: false },
8 | { where: { unsubscribeKey }}
9 | ).then(results => {
10 | // Redirect to a nicer unsubscribed page or similar
11 | // -- if results empty -> err
12 | res.status(200).send("You have been unsubscribed from this mailing list.");
13 | });
14 | }
15 |
--------------------------------------------------------------------------------
/server/controllers/template/create-template.js:
--------------------------------------------------------------------------------
1 | const db = require('../../models');
2 | const slug = require('slug');
3 |
4 | module.exports = (req, res) => {
5 |
6 | const userId = req.user.id;
7 |
8 | // Validate input
9 | if (!req.body.templateName || !req.body.type || !req.body.emailBody) {
10 | res.status(400).send();
11 | return;
12 | }
13 |
14 | const emailBodyType = req.body.emailBody;
15 |
16 | db.template.findOrCreate({
17 | where: {
18 | name: req.body.templateName,
19 | userId: userId
20 | },
21 | defaults: {
22 | name: req.body.templateName,
23 | fromName: req.body.fromName || '',
24 | fromEmail: req.body.fromEmail || '',
25 | emailSubject: req.body.emailSubject || '',
26 | emailBody: emailBodyType,
27 | trackingPixelEnabled: req.body.trackingPixelEnabled,
28 | trackLinksEnabled: req.body.trackLinksEnabled,
29 | unsubscribeLinkEnabled: req.body.unsubscribeLinkEnabled,
30 | type: req.body.type,
31 | userId: userId,
32 | slug: slug(req.body.templateName)
33 | }
34 | }).then(templateInstance => {
35 | if (templateInstance) { // Does the template already exist?
36 | res.status(400).send();
37 | } else {
38 | res.send();
39 | }
40 | }).catch(err => {
41 | throw err;
42 | });
43 | };
44 |
--------------------------------------------------------------------------------
/server/controllers/template/delete-templates.js:
--------------------------------------------------------------------------------
1 | const Template = require('../../models').template;
2 |
3 | module.exports = (req, res) => {
4 |
5 | const userId = req.user.id;
6 | const templateIds = req.body.data;
7 |
8 | Template.destroy({
9 | where: {
10 | id: { in: templateIds
11 | },
12 | userId: userId
13 | }
14 | }).then(numDeleted => {
15 | if (numDeleted) {
16 | res.send();
17 | } else {
18 | res.status(404).send();
19 | }
20 | }).catch(() => {
21 | res.status(500).send();
22 | });
23 | };
24 |
--------------------------------------------------------------------------------
/server/controllers/template/get-templates.js:
--------------------------------------------------------------------------------
1 | const Template = require('../../models').template;
2 |
3 | module.exports = (req, res) => {
4 |
5 | const userId = req.user.id;
6 |
7 | // Find all campaigns belonging to a user & send it to them
8 | Template.findAll({
9 | where: {
10 | userId: userId
11 | },
12 | raw: true
13 | }).then(instancesArray => {
14 | res.send(instancesArray);
15 | });
16 | };
17 |
--------------------------------------------------------------------------------
/server/controllers/websockets/get-profile.js:
--------------------------------------------------------------------------------
1 | const User = require('../../models').user;
2 |
3 | module.exports = function(id) {
4 | return User.findOne({
5 | where: {
6 | id: id
7 | },
8 | attributes: ['picture', 'email', 'createdAt', 'name', 'sentEmailsCount']
9 | }).then(userInstance => {
10 | const userObject = userInstance.get({ plain:true });
11 | return userObject;
12 | });
13 | };
14 |
--------------------------------------------------------------------------------
/server/controllers/websockets/send-single-notification.js:
--------------------------------------------------------------------------------
1 | module.exports = function(io, req, notification) {
2 | // Reload the session - the socket may have changed if the user refreshed their browser
3 | req.session.reload(function(err) {
4 | if (err) {
5 | console.log(err);
6 | }
7 | const socketId = req.session.passport.socket;
8 | const userSocket = io.sockets.connected[socketId];
9 | if (userSocket) {
10 | Object.assign(notification, { isUpdate: false });
11 |
12 | userSocket.emit('notification', notification);
13 | }
14 | });
15 | };
16 |
--------------------------------------------------------------------------------
/server/controllers/websockets/send-update-notification.js:
--------------------------------------------------------------------------------
1 | module.exports = function(io, req, notification) {
2 | // Reload the session - the socket may have changed if the user refreshed their browser
3 | req.session.reload(function(err) {
4 | if (err) {
5 | console.log(err);
6 | }
7 | const socketId = req.session.passport.socket;
8 | const userSocket = io.sockets.connected[socketId];
9 | if (userSocket) {
10 | Object.assign(notification, { isUpdate: true });
11 |
12 | userSocket.emit('notification', notification);
13 | }
14 | });
15 | };
16 |
--------------------------------------------------------------------------------
/server/feedback-consumer/consumer.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 | const debug = require('debug')('server:feedback-consumer:index');
3 | const redis = require('redis');
4 | const async = require('async');
5 |
6 | const feedbackConsumer = require('./feedback-consumer');
7 | const Campaign = require('../models').campaign;
8 |
9 | let redisSettings = null;
10 | if(process.env.HEROKU){
11 | redisSettings = {url: process.env.REDIS_URL}
12 | }else{
13 | redisSettings = { host: process.env.REDIS_HOST || '127.0.0.1' }
14 | }
15 |
16 |
17 | feedbackConsumer.start();
18 |
19 | console.log("Feedback consumer started: watching for feedback messages (bounces, complaints, etc) from SQS");
20 |
21 | // Restart the feedback consumers to apply new credentials when
22 | // new settings are received
23 | const subscriber = redis.createClient(redisSettings);
24 | subscriber.on('message', (channel, event) => {
25 | if (event == 'changed') {
26 | debug('Got a change-settings event, so restarting consumers to apply new credentials');
27 | feedbackConsumer.restart();
28 | }
29 | });
30 | subscriber.subscribe('change-settings');
31 |
32 | const pollingRateMs = 10000;
33 | async.forever(next => {
34 | feedbackConsumer.restart();
35 | setTimeout(next, pollingRateMs);
36 | });
37 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 |
3 | const configureServer = require('./config/server');
4 |
5 | // Use bluebird over native promises.
6 | global.Promise=require('bluebird');
7 |
8 | const server = configureServer();
9 |
10 | const PORT = process.env.PORT || 8080;
11 | server.listen(PORT, function() {
12 | const displayMessage = `
13 | ############################
14 | # Mail 4 Good started #
15 | ############################
16 | # Port: ${PORT}
17 | ############################
18 | `;
19 |
20 | console.log(displayMessage);
21 | });
22 |
--------------------------------------------------------------------------------
/server/migrations/20180521070624-pass-to-classical-auth-flow.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | up: (queryInterface, Sequelize) => {
5 | return Promise.all([
6 | queryInterface.addColumn(
7 | 'users',
8 | 'password',
9 | Sequelize.STRING
10 | ),
11 | queryInterface.addColumn(
12 | 'users',
13 | 'isAdmin',
14 | Sequelize.BOOLEAN
15 | )
16 | ])
17 | },
18 |
19 | down: (queryInterface, Sequelize) => {
20 | return Promise.all([
21 | queryInterface.removeColumn(
22 | 'users',
23 | 'password',
24 | Sequelize.STRING
25 | ),
26 | queryInterface.removeColumn(
27 | 'users',
28 | 'isAdmin',
29 | Sequelize.BOOLEAN
30 | )
31 | ])
32 | }
33 | };
34 |
--------------------------------------------------------------------------------
/server/models/acl.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | module.exports = function(sequelize, DataTypes) {
3 | var acl = sequelize.define('acl', {
4 | userId: DataTypes.STRING, // This is the userId who granted permissions
5 | fromUserEmail: DataTypes.STRING,
6 | toUserId: DataTypes.STRING,
7 | toUserEmail: DataTypes.STRING,
8 | campaigns: { type: DataTypes.STRING, validate: { isIn: [['None', 'Read', 'Write']] } },
9 | templates: { type: DataTypes.STRING, validate: { isIn: [['None', 'Read', 'Write']] } },
10 | lists: { type: DataTypes.STRING, validate: { isIn: [['None', 'Read', 'Write']] } }
11 | }, {
12 | classMethods: {
13 | associate: function() {
14 | // associations can be defined here
15 | }
16 | },
17 | indexes: [
18 | {
19 | fields:['userId']
20 | },
21 | {
22 | fields:['toUserId']
23 | }
24 | ]
25 | });
26 | return acl;
27 | };
28 |
--------------------------------------------------------------------------------
/server/models/analysis.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | module.exports = function(sequelize, DataTypes) {
3 | var analysis = sequelize.define('analysis', {
4 | name: DataTypes.STRING
5 | }, {
6 | classMethods: {
7 | associate: function(models) {
8 | // associations can be defined here
9 | analysis.belongsTo(models.user);
10 | }
11 | }
12 | });
13 | return analysis;
14 | };
15 |
--------------------------------------------------------------------------------
/server/models/analysisemail.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | module.exports = function(sequelize, DataTypes) {
3 | var analysisemail = sequelize.define('analysisEmail', {
4 | sesMessageId: DataTypes.STRING,
5 | email: DataTypes.STRING
6 | }, {
7 | classMethods: {
8 | associate: function(models) {
9 | // associations can be defined here
10 | analysisemail.belongsTo(models.analysis);
11 | }
12 | }
13 | });
14 | return analysisemail;
15 | };
16 |
--------------------------------------------------------------------------------
/server/models/campaign.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | module.exports = function(sequelize, DataTypes) {
3 | var campaign = sequelize.define('campaign', {
4 | name: DataTypes.STRING,
5 | fromName: DataTypes.STRING,
6 | fromEmail: DataTypes.STRING,
7 | emailSubject: DataTypes.STRING,
8 | emailBody: DataTypes.TEXT,
9 | type: DataTypes.STRING,
10 | slug: DataTypes.STRING,
11 | trackingPixelEnabled: { type: DataTypes.BOOLEAN, defaultValue: false },
12 | trackLinksEnabled: { type: DataTypes.BOOLEAN, defaultValue: false },
13 | unsubscribeLinkEnabled: { type: DataTypes.BOOLEAN, defaultValue: false },
14 | status: { type: DataTypes.STRING, defaultValue: 'creating' },
15 | totalCampaignSubscribers: { type: DataTypes.INTEGER, defaultValue: 0 }
16 | }, {
17 | classMethods: {
18 | associate: function(models) {
19 | // associations can be defined here
20 | campaign.belongsTo(models.user);
21 | campaign.belongsTo(models.list);
22 | campaign.hasMany(models.campaignsubscriber);
23 | campaign.hasOne(models.campaignanalytics);
24 | }
25 | }
26 | });
27 | return campaign;
28 | };
29 |
--------------------------------------------------------------------------------
/server/models/campaignanalytics.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | module.exports = function(sequelize, DataTypes) {
3 | var campaignanalytics = sequelize.define('campaignanalytics', {
4 | complaintCount: { type: DataTypes.INTEGER, defaultValue: 0 },
5 | permanentBounceCount: { type: DataTypes.INTEGER, defaultValue: 0 },
6 | transientBounceCount: { type: DataTypes.INTEGER, defaultValue: 0 },
7 | undeterminedBounceCount: { type: DataTypes.INTEGER, defaultValue: 0 },
8 | clickthroughCount: { type: DataTypes.INTEGER, defaultValue: 0 },
9 | openCount: { type: DataTypes.INTEGER, defaultValue: 0 },
10 | totalSentCount: { type: DataTypes.INTEGER, defaultValue: 0 },
11 | trackLinksEnabled: { type: DataTypes.BOOLEAN, defaultValue: true }
12 | },
13 | {
14 | freezeTableName: true, // because CampaignAnalyticss is a silly name
15 | classMethods: {
16 | associate: function(models) {
17 | campaignanalytics.belongsTo(models.campaign);
18 | campaignanalytics.hasMany(models.campaignanalyticslink);
19 | campaignanalytics.hasMany(models.campaignanalyticsopen);
20 | }
21 | }
22 | });
23 | return campaignanalytics;
24 | };
25 |
--------------------------------------------------------------------------------
/server/models/campaignanalyticslink.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | module.exports = function(sequelize, DataTypes) {
3 | var campaignanalyticslink = sequelize.define('campaignanalyticslink', {
4 | country: DataTypes.STRING, // 2 letter ISO-3166-1 country code
5 | region: DataTypes.STRING, // 2 char region code: US=ISO-3166-2, other=FIPS 10-4
6 | city: DataTypes.STRING, // Full city name
7 | ipAddress: DataTypes.STRING,
8 | operatingSystem: DataTypes.STRING,
9 | deviceType: DataTypes.STRING,
10 | deviceVendor: DataTypes.STRING,
11 | browserName: DataTypes.STRING,
12 | clicked: { type: DataTypes.BOOLEAN, defaultValue: false },
13 | trackingId: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4 }
14 | }, {
15 | classMethods: {
16 | associate: function(models) {
17 | campaignanalyticslink.belongsTo(models.campaignanalytics);
18 | campaignanalyticslink.belongsTo(models.listsubscriber);
19 | // might want to assoc with user too
20 | }
21 | }
22 | });
23 | return campaignanalyticslink;
24 | };
25 |
--------------------------------------------------------------------------------
/server/models/campaignanalyticsopen.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | module.exports = function(sequelize, DataTypes) {
3 | var campaignanalyticsopen = sequelize.define('campaignanalyticsopen', {
4 | country: DataTypes.STRING, // 2 letter ISO-3166-1 country code
5 | region: DataTypes.STRING, // 2 char region code: US=ISO-3166-2, other=FIPS 10-4
6 | city: DataTypes.STRING, // Full city name
7 | ipAddress: DataTypes.STRING,
8 | operatingSystem: DataTypes.STRING,
9 | deviceType: DataTypes.STRING,
10 | deviceVendor: DataTypes.STRING,
11 | browserName: DataTypes.STRING,
12 | opened: { type: DataTypes.BOOLEAN, defaultValue: false },
13 | trackingId: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4 }
14 | }, {
15 | classMethods: {
16 | associate: function(models) {
17 | campaignanalyticsopen.belongsTo(models.campaignanalytics);
18 | campaignanalyticsopen.belongsTo(models.listsubscriber);
19 | // might want to assoc with user too
20 | }
21 | }
22 | });
23 | return campaignanalyticsopen;
24 | };
25 |
--------------------------------------------------------------------------------
/server/models/campaignsubscriber.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | module.exports = function(sequelize, DataTypes) {
3 | var campaignsubscriber = sequelize.define('campaignsubscriber', {
4 | email: DataTypes.STRING,
5 | messageId: DataTypes.STRING,
6 | status: DataTypes.STRING,
7 | bounceType: DataTypes.STRING,
8 | bounceSubType: DataTypes.STRING,
9 | sent: { type: DataTypes.BOOLEAN, defaultValue: false }
10 | }, {
11 | classMethods: {
12 | associate: function(models) {
13 | campaignsubscriber.belongsTo(models.campaign, { foreignKeyConstraint: true });
14 | campaignsubscriber.belongsTo(models.listsubscriber, { foreignKeyConstraint: true });
15 | }
16 | },
17 | indexes: [
18 | {
19 | fields:['email']
20 | },
21 | {
22 | fields:['sent']
23 | },
24 | {
25 | fields:['listsubscriberId']
26 | },
27 | {
28 | fields:['campaignId']
29 | }
30 | ]
31 | });
32 | return campaignsubscriber;
33 | };
34 |
--------------------------------------------------------------------------------
/server/models/list.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | module.exports = function(sequelize, DataTypes) {
3 | var list = sequelize.define('list', {
4 | name: DataTypes.STRING,
5 | subscribeKey: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4 },
6 | additionalFields: { type: DataTypes.ARRAY(DataTypes.STRING), defaultValue: [ ] },
7 | status: { type: DataTypes.STRING, defaultValue: 'processing', validate: { isIn: [['processing', 'ready']] } },
8 | total: { type: DataTypes.INTEGER, defaultValue: 0 },
9 | }, {
10 | classMethods: {
11 | associate: function(models) {
12 | // associations can be defined here
13 | list.belongsTo(models.user);
14 | }
15 | }
16 | });
17 | return list;
18 | };
19 |
--------------------------------------------------------------------------------
/server/models/listsubscriber.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | module.exports = function(sequelize, DataTypes) {
3 | var listsubscriber = sequelize.define('listsubscriber', {
4 | email: DataTypes.STRING,
5 | subscribed: { type: DataTypes.BOOLEAN, defaultValue: true },
6 | unsubscribeKey: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4 },
7 | mostRecentStatus: { type: DataTypes.STRING, defaultValue: 'unconfirmed' }, // bounce:permanent, bounce:transient, complaint
8 | additionalData: { type: DataTypes.JSONB, defaultValue: {} }
9 | }, {
10 | classMethods: {
11 | associate: function(models) {
12 | // associations can be defined here
13 | listsubscriber.belongsTo(models.list);
14 | listsubscriber.hasMany(models.campaignanalyticslink);
15 | listsubscriber.hasMany(models.campaignanalyticsopen);
16 | listsubscriber.hasMany(models.campaignsubscriber);
17 | }
18 | },
19 | indexes: [
20 | {
21 | fields: ['email']
22 | },
23 | {
24 | fields: ['subscribed']
25 | }
26 | ]
27 | });
28 | return listsubscriber;
29 | };
30 |
--------------------------------------------------------------------------------
/server/models/offerPermission.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | module.exports = function(sequelize, DataTypes) {
3 | var offerPermission = sequelize.define('offerPermission', {
4 | toUserId: DataTypes.STRING,
5 | fromUserEmail: DataTypes.STRING,
6 | toUserEmail: DataTypes.STRING,
7 | campaigns: { type: DataTypes.STRING, validate: { isIn: [['None', 'Read', 'Write']] } },
8 | templates: { type: DataTypes.STRING, validate: { isIn: [['None', 'Read', 'Write']] } },
9 | lists: { type: DataTypes.STRING, validate: { isIn: [['None', 'Read', 'Write']] } }
10 | }, {
11 | classMethods: {
12 | associate: function(models) {
13 | // associations can be defined here
14 | offerPermission.belongsTo(models.user);
15 | }
16 | }
17 | });
18 | return offerPermission;
19 | };
20 |
--------------------------------------------------------------------------------
/server/models/subscriber.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | module.exports = function(sequelize, DataTypes) {
3 | var subscriber = sequelize.define('subscriber', {
4 | email: DataTypes.STRING,
5 | customdata: DataTypes.ARRAY(DataTypes.STRING)
6 | }, {
7 | classMethods: {
8 | associate: function(models) {
9 | // associations can be defined here
10 | },
11 | indexes: [
12 | {
13 | fields:['email']
14 | }
15 | ]
16 | }
17 | });
18 | return subscriber;
19 | };
20 |
--------------------------------------------------------------------------------
/server/models/template.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | module.exports = function(sequelize, DataTypes) {
3 | var template = sequelize.define('template', {
4 | name: DataTypes.STRING,
5 | fromName: DataTypes.STRING,
6 | fromEmail: DataTypes.STRING,
7 | emailSubject: DataTypes.STRING,
8 | emailBody: DataTypes.TEXT,
9 | type: DataTypes.STRING,
10 | slug: DataTypes.STRING,
11 | trackingPixelEnabled: { type: DataTypes.BOOLEAN, defaultValue: false },
12 | trackLinksEnabled: { type: DataTypes.BOOLEAN, defaultValue: false },
13 | unsubscribeLinkEnabled: { type: DataTypes.BOOLEAN, defaultValue: false }
14 | }, {
15 | classMethods: {
16 | associate: function(models) {
17 | // associations can be defined here
18 | template.belongsTo(models.user);
19 | }
20 | }
21 | });
22 | return template;
23 | };
24 |
--------------------------------------------------------------------------------
/server/routes/accountsManagement.js:
--------------------------------------------------------------------------------
1 | const bodyParser = require('body-parser');
2 | const parseJson = bodyParser.json();
3 |
4 | const createUser = require('../controllers/accountsManagement/create-user');
5 | const deleteUser = require('../controllers/accountsManagement/delete-user');
6 |
7 | // Middleware
8 | const { apiIsAuth } = require('./middleware/auth');
9 |
10 | module.exports = function(app) {
11 | app.post('/api/create-user', apiIsAuth, parseJson, (req, res) => {
12 | createUser(req,res);
13 | })
14 |
15 | app.delete('/api/delete-user', apiIsAuth, parseJson, (req,res) => {
16 | deleteUser(req,res)
17 | });
18 | };
19 |
--------------------------------------------------------------------------------
/server/routes/middleware/auth.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | /* Helper functions for verifying authentication */
3 | // Check user is allowed to load SPA
4 | isAuth(req, res, next) {
5 | if (req.isAuthenticated()) {
6 | return next();
7 | } else {
8 | res.redirect('/login');
9 | }
10 | },
11 | // Check user accessing API route is authenticated
12 | apiIsAuth(req, res, next) {
13 | if (req.isAuthenticated()) {
14 | return next();
15 | } else {
16 | res.status(403).send();
17 | }
18 | }
19 | };
20 |
--------------------------------------------------------------------------------
/server/tests/controllers/lists/test-csv-files/10emailswithtoomanycommas:
--------------------------------------------------------------------------------
1 | email
2 | a@a.com,,,
3 | b@b.com,
4 | c@c.com
5 |
--------------------------------------------------------------------------------
/server/tests/mocha.opts:
--------------------------------------------------------------------------------
1 | -r jsdom-global/register
2 |
--------------------------------------------------------------------------------
/tools/chalkConfig.js:
--------------------------------------------------------------------------------
1 | // Centralized configuration for chalk, which is used to add color to console.log statements.
2 | import chalk from 'chalk';
3 | export const chalkError = chalk.red;
4 | export const chalkSuccess = chalk.green;
5 | export const chalkWarning = chalk.yellow;
6 | export const chalkProcessing = chalk.blue;
7 |
--------------------------------------------------------------------------------
/tools/testClientSetup.js:
--------------------------------------------------------------------------------
1 | require('jsdom-global')();
2 |
--------------------------------------------------------------------------------
/tools/testSetup.js:
--------------------------------------------------------------------------------
1 | // Tests are placed alongside files under test.
2 | // This file does the following:
3 | // 1. Sets the environment to 'test' so that
4 | // dev-specific babel config in .babelrc doesn't run.
5 | // 2. Disables Webpack-specific features that Mocha doesn't understand.
6 | // 3. Registers babel for transpiling our code for testing.
7 |
8 | // This assures the .babelrc dev config (which includes
9 | // hot module reloading code) doesn't apply for tests.
10 | // Setting NODE_ENV to test instead of production because setting it to production will suppress error messaging
11 | // and propType validation warnings.
12 | require('dotenv').config();
13 | require("babel-polyfill");
14 | process.env.NODE_ENV = 'test';
15 |
16 | // Disable webpack-specific features for tests since
17 | // Mocha doesn't know what to do with them.
18 | ['.css', '.scss', '.png', '.jpg'].forEach(ext => {
19 | require.extensions[ext] = () => null;
20 | });
21 |
22 | // Register babel so that it will transpile ES6 to ES5
23 | // before our tests run.
24 | require('babel-register')();
25 |
--------------------------------------------------------------------------------
/utility/generateEmailCsv.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import random, string
3 |
4 | try:
5 | numberOfEmailsToGenerate = int(sys.argv[1])
6 | print('Generating a CSV with %d random emails' % numberOfEmailsToGenerate)
7 | print('This may take some time if the CSV is large ...')
8 | except:
9 | sys.exit('Failed to parse argument. Usage "python %s ' % sys.argv[0])
10 |
11 | with open("./generated.csv", "w") as generated_csv:
12 | for x in range(0, numberOfEmailsToGenerate):
13 | randomString = ''.join(random.choice(string.lowercase) for i in range(20))
14 | generated_csv.write("%s@gmail.com\n" % randomString)
15 |
--------------------------------------------------------------------------------