├── log └── .keep ├── app ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── concerns │ │ └── .keep │ ├── acme_challenge.rb │ ├── meta_value.rb │ ├── application_record.rb │ ├── team.rb │ ├── deny_list.rb │ ├── click_event.rb │ ├── link.rb │ ├── open_event.rb │ ├── delivery_link.rb │ └── address.rb ├── graphql │ ├── types │ │ ├── .keep │ │ ├── dnsbl.rb │ │ ├── family_and_version.rb │ │ ├── open_event.rb │ │ ├── key_value.rb │ │ ├── blocked_address_connection.rb │ │ ├── click_event.rb │ │ ├── date_time.rb │ │ ├── blocked_address_permissions.rb │ │ ├── key_value_attributes.rb │ │ ├── count.rb │ │ ├── user_error.rb │ │ ├── app_permissions.rb │ │ ├── status.rb │ │ ├── email_content.rb │ │ ├── base_connection.rb │ │ ├── ip_info.rb │ │ ├── email_connection.rb │ │ ├── team.rb │ │ ├── delivery_event.rb │ │ └── smtp_server.rb │ ├── mutations │ │ ├── .keep │ │ ├── remove_app.rb │ │ ├── remove_admin.rb │ │ ├── upgrade_app_dkim.rb │ │ ├── send_reset_password_instructions.rb │ │ ├── invite_admin_to_team.rb │ │ ├── login_admin.rb │ │ ├── remove_blocked_address.rb │ │ ├── invite_team.rb │ │ ├── base.rb │ │ ├── reset_password_by_token.rb │ │ └── accept_admin_invitation.rb │ └── cuttlefish_schema.rb ├── controllers │ ├── concerns │ │ └── .keep │ ├── landing_controller.rb │ ├── acme_challenges_controller.rb │ ├── documentation_controller.rb │ ├── clients_controller.rb │ ├── admins_controller.rb │ ├── teams_controller.rb │ ├── domains_controller.rb │ └── main_controller.rb ├── views │ ├── test_mailer │ │ ├── test_email.text.erb │ │ └── test_email.html.haml │ ├── main │ │ ├── reputation.html.haml │ │ ├── index.html.haml │ │ ├── _status_counts.html.haml │ │ └── _reputation.html.haml │ ├── documentation │ │ ├── _other.html.haml │ │ ├── _django.html.haml │ │ ├── _general.html.haml │ │ └── _rails.html.haml │ ├── devise │ │ ├── mailer │ │ │ ├── confirmation_instructions.html.erb │ │ │ ├── unlock_instructions.html.erb │ │ │ ├── reset_password_instructions.html.erb │ │ │ └── invitation_instructions.html.erb │ │ ├── shared │ │ │ └── _links.html.erb │ │ ├── unlocks │ │ │ └── new.html.erb │ │ └── confirmations │ │ │ └── new.html.erb │ ├── deliveries │ │ ├── _postfix_log_lines.html.haml │ │ ├── _open_events.html.haml │ │ ├── _from.html.haml │ │ ├── _click_events.html.haml │ │ └── _to.html.haml │ ├── addresses │ │ └── from.html.haml │ ├── admins │ │ ├── passwords │ │ │ ├── new.html.haml │ │ │ └── edit.html.erb │ │ ├── index.html.haml │ │ ├── sessions │ │ │ └── new.html.haml │ │ ├── _admin.html.haml │ │ └── registrations │ │ │ └── new.html.haml │ ├── domains │ │ └── index.html.haml │ ├── invitations │ │ └── edit.html.haml │ ├── shared │ │ └── _footer.html.haml │ ├── apps │ │ ├── index.html.haml │ │ └── new.html.haml │ ├── clients │ │ └── index.html.haml │ └── teams │ │ └── index.html.haml ├── assets │ ├── javascripts │ │ ├── emails.js │ │ ├── main.js │ │ ├── documentation.js │ │ ├── apps.js │ │ └── application.js │ ├── stylesheets │ │ ├── apps.css.scss │ │ ├── coderay.css.scss │ │ ├── application.css │ │ └── emails.css.scss │ └── images │ │ ├── cuttlefish.ico │ │ ├── cuttlefish.png │ │ ├── screenshots │ │ ├── 1.png │ │ ├── 2.png │ │ └── 3.png │ │ └── cuttlefish_80x48.png ├── helpers │ ├── admins_helper.rb │ ├── deny_lists_helper.rb │ ├── apps_helper.rb │ ├── main_helper.rb │ └── deliveries_helper.rb ├── policies │ ├── test_email_policy.rb │ ├── invitation_policy.rb │ ├── meta_value_policy.rb │ ├── registration_policy.rb │ ├── deny_list_policy.rb │ ├── admin_policy.rb │ ├── team_policy.rb │ ├── application_policy.rb │ ├── app_policy.rb │ └── delivery_policy.rb ├── forms │ ├── admin_form.rb │ └── app_form.rb └── services │ ├── app_services │ ├── destroy.rb │ ├── upgrade_dkim.rb │ └── setup_custom_tracking_domain_ssl.rb │ ├── webhook_services │ └── post_test_event.rb │ ├── admin_services │ └── destroy.rb │ ├── deny_list_services │ └── destroy.rb │ └── application_service.rb ├── lib ├── assets │ └── .keep ├── tasks │ └── .keep ├── api │ ├── apps │ │ ├── new.graphql │ │ ├── toggle_dkim_query.graphql │ │ ├── upgrade_dkim.graphql │ │ ├── destroy.graphql │ │ ├── webhook.graphql │ │ ├── index.graphql │ │ ├── edit.graphql │ │ ├── create.graphql │ │ ├── update.graphql │ │ ├── dkim.graphql │ │ └── show.graphql │ ├── main │ │ ├── index.graphql │ │ ├── reputation.graphql │ │ ├── partial.graphql │ │ └── status_counts.graphql │ ├── sessions │ │ ├── new.graphql │ │ └── create.graphql │ ├── invitations │ │ ├── new.graphql │ │ ├── create.graphql │ │ ├── create_error.graphql │ │ └── update.graphql │ ├── registrations │ │ ├── new.graphql │ │ ├── update_error.graphql │ │ ├── edit.graphql │ │ ├── destroy.graphql │ │ ├── create.graphql │ │ └── update.graphql │ ├── admins │ │ ├── destroy.graphql │ │ └── index.graphql │ ├── deny_lists │ │ ├── destroy.graphql │ │ └── index.graphql │ ├── test_emails │ │ ├── new.graphql │ │ └── create.graphql │ ├── deliveries │ │ ├── html.graphql │ │ └── index.graphql │ ├── teams │ │ ├── invite.graphql │ │ └── index.graphql │ ├── passwords │ │ ├── create.graphql │ │ └── update.graphql │ ├── documentation │ │ └── index.graphql │ ├── clients │ │ └── index.graphql │ ├── domains │ │ └── index.graphql │ └── addresses │ │ ├── from.graphql │ │ └── to.graphql ├── filters │ ├── base.rb │ ├── mailer_header.rb │ └── inline_css.rb ├── setup_custom_tracking_domain_ssl_worker.rb ├── post_delivery_event_worker.rb ├── hash_id.rb ├── reputation.rb ├── cuttlefish_log_daemon.rb ├── parse_headers_create_email_worker.rb ├── pager_renderer.rb ├── bootstrap_link_renderer.rb └── user_agent.rb ├── public ├── favicon.ico ├── robots.txt ├── 422.html ├── 500.html └── 404.html ├── .ruby-version ├── postfix_log └── supervisor │ └── .keep ├── vendor └── assets │ ├── javascripts │ ├── .keep │ └── bootstrap-extended │ │ └── bootstrap-rowlink.min.js │ └── stylesheets │ ├── .keep │ └── bootstrap-extended │ └── bootstrap-rowlink.min.css ├── .rspec ├── provisioning ├── hosts ├── roles │ ├── cuttlefish-backup │ │ ├── tests │ │ │ ├── inventory │ │ │ └── test.yml │ │ ├── vars │ │ │ └── main.yml │ │ ├── defaults │ │ │ └── main.yml │ │ ├── handlers │ │ │ └── main.yml │ │ ├── .travis.yml │ │ └── tasks │ │ │ └── main.yml │ ├── cuttlefish-postfix │ │ ├── vars │ │ │ └── main.yml │ │ ├── defaults │ │ │ └── main.yml │ │ └── handlers │ │ │ └── main.yml │ ├── cuttlefish-app │ │ ├── defaults │ │ │ └── main.yml │ │ ├── handlers │ │ │ └── main.yml │ │ └── templates │ │ │ ├── database.yml │ │ │ ├── copy-cuttlefish-daemon-certs.sh │ │ │ └── env │ └── deploy-user │ │ ├── handlers │ │ └── main.yml │ │ └── tasks │ │ └── main.yml ├── requirements.txt ├── requirements.yml └── playbook.yml ├── .dockerignore ├── provision_production.sh ├── Capfile ├── config ├── database.travis.yml ├── initializers │ ├── google_analytics.rb │ ├── mime_types.rb │ ├── session_store.rb │ ├── filter_parameter_logging.rb │ ├── application_controller_renderer.rb │ ├── cookies_serializer.rb │ ├── graphiql.rb │ ├── app_version.rb │ ├── permissions_policy.rb │ ├── wrap_parameters.rb │ ├── backtrace_silencers.rb │ ├── assets.rb │ ├── formtastic.rb │ └── inflections.rb ├── spring.rb ├── boot.rb ├── environment.rb ├── cable.yml ├── database.yml ├── locales │ ├── en.bootstrap.yml │ ├── devise_invitable.en.yml │ └── en.yml └── secrets.yml ├── spec ├── factories │ ├── open_events.rb │ ├── apps.rb │ ├── addresses.rb │ ├── meta_values.rb │ ├── deliveries.rb │ ├── teams.rb │ ├── links.rb │ ├── admins.rb │ ├── delivery_links.rb │ ├── deny_lists.rb │ ├── click_events.rb │ ├── postfix_log_lines.rb │ └── emails.rb ├── fixtures │ └── archive │ │ └── 2014-06-04.tar.gz ├── models │ ├── acme_challenge_spec.rb │ └── delivery_link_spec.rb ├── lib │ ├── filters │ │ ├── base_spec.rb │ │ ├── master_spec.rb │ │ └── mailer_header_spec.rb │ ├── hash_id_spec.rb │ └── dkim_dns_spec.rb ├── routing │ └── deliveries_routing_spec.rb ├── services │ └── webhook_services │ │ └── post_test_event_spec.rb ├── helpers │ └── admins_helper_spec.rb ├── support │ └── matchers │ │ └── permit_matcher.rb ├── controllers │ ├── landing_controller_spec.rb │ ├── admins │ │ └── sessions_controller_spec.rb │ └── invitations_controller_spec.rb └── policies │ ├── test_email_policy_spec.rb │ └── invitation_policy_spec.rb ├── bin ├── bundle ├── rake ├── delayed_job ├── rails ├── rspec ├── spring ├── yarn └── update ├── Procfile ├── ansible.cfg ├── config.ru ├── db └── migrate │ ├── 20200901065830_add_key_index_to_meta_values.rb │ ├── 20200912055441_rename_app_deny_lists.rb │ ├── 20201110042853_remove_admins_api_key.rb │ ├── 20200902023500_revert_add_key_index_to_meta_values.rb │ ├── 20130709001223_add_url_index_to_links.rb │ ├── 20130407044432_rename_table.rb │ ├── 20130414064809_add_index_to_addresses.rb │ ├── 20141130220259_add_name_to_admins.rb │ ├── 20200902042840_remove_limit_on_url_length.rb │ ├── 20130425025753_add_app_id_to_emails.rb │ ├── 20130712034237_rename_link_events.rb │ ├── 20141228101710_remove_url_from_apps.rb │ ├── 20130420061331_add_ip_to_open_events.rb │ ├── 20130617000225_remove_status_from_emails.rb │ ├── 20130710003548_add_subject_to_emails.rb │ ├── 20130407185455_add_message_id_to_emails.rb │ ├── 20130407192417_add_data_hash_to_emails.rb │ ├── 20130410050345_add_index_delivered_to_emails.rb │ ├── 20130424051134_add_index_to_deliveries.rb │ ├── 20140713004313_add_from_domain_to_apps.rb │ ├── 20180904022131_rename_black_lists_table.rb │ ├── 20130410043016_add_index_created_at_in_emails.rb │ ├── 20130418095518_add_referer_to_open_events.rb │ ├── 20141130114112_add_app_id_index_to_deliveries.rb │ ├── 20130407053630_rename_address_in_addresses.rb │ ├── 20130420092059_add_combined_index_on_emails.rb │ ├── 20130410151634_remove_delivered_from_emails.rb │ ├── 20130416043210_add_user_agent_to_open_events.rb │ ├── 20140714022048_remove_dkim_public_key_from_apps.rb │ ├── 20141222054155_allow_null_team_id_in_apps.rb │ ├── 20180921001452_rename_super_admin_column.rb │ ├── 20130405045356_add_postfix_queue_id_to_emails.rb │ ├── 20130405081503_add_time_to_postfix_log_lines.rb │ ├── 20130410083333_add_index_delivery_status_to_emails.rb │ ├── 20130429005659_add_deliver_id_index_to_open_events.rb │ ├── 20130607022711_rename_cuttlefish_field_in_apps.rb │ ├── 20141126052536_create_teams.rb │ ├── 20200913012801_remove_caused_by_delivery_id_from_deny_lists.rb │ ├── 20240624092819_add_disable_css_inlining_to_emails.rb │ ├── 20130406061755_remove_not_delivered_from_emails.rb │ ├── 20130410160315_rename_delivery_status_in_emails.rb │ ├── 20130425050658_add_cuttlefish_to_apps.rb │ ├── 20130430003245_add_another_index_to_deliveries.rb │ ├── 20240624093627_remove_default_on_disable_css_inlining.rb │ ├── 20130427055637_add_index_to_deliveries_join_table.rb │ ├── 20130618021807_add_address_id_index_to_deliveries.rb │ ├── 20140714004505_add_dkim_enabled_to_apps.rb │ ├── 20130410000152_add_indexes_to_tables.rb │ ├── 20130416050249_add_open_tracked_hash_to_deliveries.rb │ ├── 20130430053701_add_composite_index_to_deliveries.rb │ ├── 20141201051257_add_super_admin_to_admins.rb │ ├── 20130406074059_run_update_deliver_status.rb │ ├── 20130410032907_remove_email_id_from_postfix_log_lines.rb │ ├── 20130524114642_remove_open_tracked_hash_from_deliveries.rb │ ├── 20130712062539_rename_link_tracking_enabled.rb │ ├── 20141118012335_remove_default_app_from_apps.rb │ ├── 20141222053542_add_cuttlefish_to_apps_again.rb │ ├── 20181011045125_remove_smtp_password_locked_field_from_app.rb │ ├── 20130425022022_add_password_locked_to_apps.rb │ ├── 20130430032442_add_composite_index_to_postfix_log_lines.rb │ ├── 20130412070256_change_default_status_of_emails.rb │ ├── 20130416012515_add_open_tracked_to_deliveries.rb │ ├── 20130610071554_fix_foreign_keys.rb │ ├── 20130406040719_add_delivered_to_emails.rb │ ├── 20130425023811_rename_name_fields_in_apps.rb │ ├── 20130602040058_add_open_tracking_enabled_to_apps.rb │ ├── 20130605065600_create_links.rb │ ├── 20140713001257_add_dkim_key_pair_to_apps.rb │ ├── 20220606034659_add_custom_tracking_domain_ssl_enabled_to_apps.rb │ ├── 20130330230840_add_email_address_id_to_email.rb │ ├── 20141124001230_add_archived_deliveries_count_to_apps.rb │ ├── 20130330193042_create_emails.rb │ ├── 20130416010949_create_open_events.rb │ ├── 20130330222744_create_email_addresses.rb │ ├── 20130410152558_add_default_to_delivery_status_in_emails.rb │ ├── 20141107045312_fix_invitation_for_admins.rb │ ├── 20130407045750_rename_table_email_addresses.rb │ ├── 20220601230818_create_acme_challenges.rb │ ├── 20200812050040_add_ignore_deny_list_to_emails.rb │ ├── 20141130113657_update_app_id_in_deliveries.rb │ ├── 20130405071909_create_postfix_log_lines.rb │ ├── 20130705080631_add_indexes_to_join_tables.rb │ ├── 20140716032348_create_black_lists.rb │ ├── 20141118070152_add_foreign_key_constraints_to_emails.rb │ ├── 20130418054241_user_agent_should_be_text.rb │ ├── 20130429013050_add_various_indexes.rb │ ├── 20130602070927_add_link_tracking_enabled_to_apps.rb │ ├── 20130605070327_create_delivery_links.rb │ ├── 20140702031017_change_handler_to_medium_text_on_delayed_jobs.rb │ ├── 20130410082419_add_delivery_status_to_emails.rb │ ├── 20130410153952_cleanup_postfix_log_lines.rb │ ├── 20130405184644_convert_string_to_text_in_postfix_log_lines.rb │ ├── 20130430235232_tweak_postfix_log_lines_index_order.rb │ ├── 20130606060336_create_link_events.rb │ ├── 20200818194827_create_meta_values.rb │ ├── 20130401040355_add_to_association_to_emails.rb │ ├── 20130424060803_create_apps.rb │ ├── 20141128055957_add_app_id_to_deliveries.rb │ ├── 20200903034601_add_app_deny_lists.rb │ ├── 20200912053221_drop_deny_lists.rb │ ├── 20130412020227_add_sent_to_deliveries.rb │ ├── 20130514163825_rename_cuttlefish_app.rb │ ├── 20180723075018_add_api_key_to_admins.rb │ ├── 20130430001018_fill_in_empty_deliveries_created_at_times.rb │ ├── 20141118005010_remove_settings_table.rb │ ├── 20130616063653_add_status_to_deliveries.rb │ ├── 20141127000723_add_team_id_to_apps.rb │ ├── 20130414051352_add_postfix_queue_id_to_deliveries.rb │ ├── 20180816045458_add_legacy_dkim_selector_to_apps.rb │ ├── 20141128073822_add_team_id_to_black_lists.rb │ ├── 20130514151224_get_rid_of_nil_app.rb │ ├── 20130418045343_add_counter_cache_to_deliveries.rb │ ├── 20140716040430_populate_black_lists.rb │ ├── 20130502055816_create_settings.rb │ ├── 20141118070603_add_foreign_key_constraints_to_postfix_log_lines.rb │ ├── 20130410021031_add_delivery_id_to_postfix_log_lines.rb │ ├── 20140711054108_add_counter_cache_to_delivery_links.rb │ ├── 20200825041009_add_webhook_to_apps.rb │ ├── 20150112214714_drop_delayed_jobs_table.rb │ ├── 20130616043406_add_null_constraints_to_postfix_log_lines.rb │ ├── 20141126054742_add_team_id_to_admins.rb │ └── 20130419081443_devise_invitable_add_to_admins.rb ├── dockerfiles ├── mailcatcher └── web ├── Procfile.production ├── Rakefile ├── .travis.yml └── .devcontainer └── docker-compose.yml /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/graphql/types/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | ruby-3.0.6 2 | -------------------------------------------------------------------------------- /app/graphql/mutations/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /postfix_log/supervisor/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color --require spec_helper 2 | -------------------------------------------------------------------------------- /app/views/test_mailer/test_email.text.erb: -------------------------------------------------------------------------------- 1 | <%= @text %> -------------------------------------------------------------------------------- /provisioning/hosts: -------------------------------------------------------------------------------- 1 | li743-35.members.linode.com 2 | -------------------------------------------------------------------------------- /app/assets/javascripts/emails.js: -------------------------------------------------------------------------------- 1 | $('td.status').tooltip(); 2 | -------------------------------------------------------------------------------- /app/views/test_mailer/test_email.html.haml: -------------------------------------------------------------------------------- 1 | = simple_format @text -------------------------------------------------------------------------------- /provisioning/roles/cuttlefish-backup/tests/inventory: -------------------------------------------------------------------------------- 1 | localhost -------------------------------------------------------------------------------- /provisioning/roles/cuttlefish-backup/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # vars file for backup 3 | -------------------------------------------------------------------------------- /provisioning/roles/cuttlefish-postfix/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # vars file for postfix 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git* 2 | log/* 3 | tmp/* 4 | provisioning/* 5 | Dockerfile 6 | README.md 7 | -------------------------------------------------------------------------------- /lib/api/apps/new.graphql: -------------------------------------------------------------------------------- 1 | { 2 | viewer { 3 | email 4 | siteAdmin 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /lib/api/main/index.graphql: -------------------------------------------------------------------------------- 1 | { 2 | viewer { 3 | email 4 | siteAdmin 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /lib/api/sessions/new.graphql: -------------------------------------------------------------------------------- 1 | { 2 | configuration { 3 | freshInstall 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /provisioning/roles/cuttlefish-backup/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # defaults file for backup 3 | -------------------------------------------------------------------------------- /provisioning/roles/cuttlefish-backup/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # handlers file for backup 3 | -------------------------------------------------------------------------------- /lib/api/invitations/new.graphql: -------------------------------------------------------------------------------- 1 | { 2 | viewer { 3 | email 4 | siteAdmin 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /lib/api/main/reputation.graphql: -------------------------------------------------------------------------------- 1 | { 2 | viewer { 3 | email 4 | siteAdmin 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /lib/api/registrations/new.graphql: -------------------------------------------------------------------------------- 1 | { 2 | configuration { 3 | freshInstall 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /provisioning/roles/cuttlefish-app/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # defaults file for cuttlefish-app 3 | -------------------------------------------------------------------------------- /provisioning/roles/cuttlefish-postfix/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # defaults file for postfix 3 | -------------------------------------------------------------------------------- /provision_production.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ansible-playbook -i provisioning/hosts provisioning/playbook.yml 3 | -------------------------------------------------------------------------------- /lib/api/registrations/update_error.graphql: -------------------------------------------------------------------------------- 1 | { 2 | viewer { 3 | email 4 | siteAdmin 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Capfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | load "deploy" 4 | load "deploy/assets" 5 | load "config/deploy" 6 | -------------------------------------------------------------------------------- /app/assets/stylesheets/apps.css.scss: -------------------------------------------------------------------------------- 1 | table.dns { 2 | table-layout: fixed; 3 | word-wrap: break-word; 4 | } 5 | -------------------------------------------------------------------------------- /lib/api/apps/toggle_dkim_query.graphql: -------------------------------------------------------------------------------- 1 | query($id: ID!) { 2 | app(id: $id) { 3 | dkimEnabled 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /lib/api/registrations/edit.graphql: -------------------------------------------------------------------------------- 1 | { 2 | viewer { 3 | email 4 | name 5 | siteAdmin 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/assets/images/cuttlefish.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlandauer/cuttlefish/HEAD/app/assets/images/cuttlefish.ico -------------------------------------------------------------------------------- /app/assets/images/cuttlefish.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlandauer/cuttlefish/HEAD/app/assets/images/cuttlefish.png -------------------------------------------------------------------------------- /app/models/acme_challenge.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AcmeChallenge < ApplicationRecord 4 | end 5 | -------------------------------------------------------------------------------- /config/database.travis.yml: -------------------------------------------------------------------------------- 1 | test: 2 | database: cuttlefish_test 3 | adapter: postgresql 4 | username: postgres 5 | -------------------------------------------------------------------------------- /provisioning/roles/deploy-user/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: sshd restart 3 | service: name=ssh state=restarted 4 | -------------------------------------------------------------------------------- /app/assets/images/screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlandauer/cuttlefish/HEAD/app/assets/images/screenshots/1.png -------------------------------------------------------------------------------- /app/assets/images/screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlandauer/cuttlefish/HEAD/app/assets/images/screenshots/2.png -------------------------------------------------------------------------------- /app/assets/images/screenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlandauer/cuttlefish/HEAD/app/assets/images/screenshots/3.png -------------------------------------------------------------------------------- /config/initializers/google_analytics.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | GA.tracker = ENV["GOOGLE_ANALYTICS_CODE"] 4 | -------------------------------------------------------------------------------- /spec/factories/open_events.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :open_event 5 | end 6 | -------------------------------------------------------------------------------- /app/assets/images/cuttlefish_80x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlandauer/cuttlefish/HEAD/app/assets/images/cuttlefish_80x48.png -------------------------------------------------------------------------------- /provisioning/roles/cuttlefish-backup/tests/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: localhost 3 | remote_user: root 4 | roles: 5 | - backup -------------------------------------------------------------------------------- /app/models/meta_value.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class MetaValue < ApplicationRecord 4 | belongs_to :email 5 | end 6 | -------------------------------------------------------------------------------- /config/spring.rb: -------------------------------------------------------------------------------- 1 | Spring.watch( 2 | ".ruby-version", 3 | ".rbenv-vars", 4 | "tmp/restart.txt", 5 | "tmp/caching-dev.txt" 6 | ) 7 | -------------------------------------------------------------------------------- /spec/fixtures/archive/2014-06-04.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlandauer/cuttlefish/HEAD/spec/fixtures/archive/2014-06-04.tar.gz -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /lib/api/admins/destroy.graphql: -------------------------------------------------------------------------------- 1 | mutation ($id: ID!) { 2 | removeAdmin(id: $id) { 3 | admin { 4 | displayName 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lib/api/main/partial.graphql: -------------------------------------------------------------------------------- 1 | { 2 | dnsbl { 3 | dnsbl 4 | meaning 5 | } 6 | configuration { 7 | ipAddress 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | smtp: bundle exec rake cuttlefish:smtp 2 | log: bundle exec rake cuttlefish:log 3 | sidekiq: bundle exec sidekiq 4 | web: bundle exec rails s 5 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 2 | 3 | require "bundler/setup" # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /lib/api/apps/upgrade_dkim.graphql: -------------------------------------------------------------------------------- 1 | mutation($id: ID!) { 2 | upgradeAppDkim(id: $id) { 3 | app { 4 | id 5 | name 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/api/registrations/destroy.graphql: -------------------------------------------------------------------------------- 1 | mutation ($id: ID!) { 2 | removeAdmin(id: $id) { 3 | admin { 4 | displayName 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationRecord < ActiveRecord::Base 4 | self.abstract_class = true 5 | end 6 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | load File.expand_path("spring", __dir__) 3 | require_relative "../config/boot" 4 | require "rake" 5 | Rake.application.run 6 | -------------------------------------------------------------------------------- /lib/api/deny_lists/destroy.graphql: -------------------------------------------------------------------------------- 1 | mutation($id: ID!) { 2 | removeBlockedAddress(id: $id) { 3 | blockedAddress { 4 | address 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lib/api/test_emails/new.graphql: -------------------------------------------------------------------------------- 1 | { 2 | apps { 3 | id 4 | name 5 | } 6 | viewer { 7 | name 8 | email 9 | siteAdmin 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /spec/factories/apps.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :app do 5 | team 6 | name { "My App" } 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative "application" 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /spec/factories/addresses.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :address do 5 | text { "matthew@foo.com" } 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/api/apps/destroy.graphql: -------------------------------------------------------------------------------- 1 | mutation($id: ID!) { 2 | removeApp(id: $id) { 3 | errors { 4 | message 5 | path 6 | type 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/models/team.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Team < ApplicationRecord 4 | has_many :admins, dependent: :destroy 5 | has_many :apps, dependent: :destroy 6 | end 7 | -------------------------------------------------------------------------------- /spec/factories/meta_values.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :meta_value do 5 | key { "foo" } 6 | value { "bar" } 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | vault_password_file = ~/.cuttlefish_ansible_vault_pass.txt 3 | roles_path = provisioning/roles 4 | remote_user = root 5 | [ssh_connection] 6 | pipelining = True 7 | -------------------------------------------------------------------------------- /app/assets/javascripts/main.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | $("#status-counts").load("/status_counts", () => $('tbody').rowlink()); 3 | }); 4 | 5 | $(() => $("#reputation").load("/reputation")); 6 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative "config/environment" 4 | 5 | run Rails.application 6 | Rails.application.load_server 7 | -------------------------------------------------------------------------------- /spec/factories/deliveries.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :delivery do 5 | app 6 | email 7 | address 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/factories/teams.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Read about factories at https://github.com/thoughtbot/factory_girl 4 | 5 | FactoryBot.define do 6 | factory :team 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20200901065830_add_key_index_to_meta_values.rb: -------------------------------------------------------------------------------- 1 | class AddKeyIndexToMetaValues < ActiveRecord::Migration[5.2] 2 | def change 3 | add_index :meta_values, :key 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20200912055441_rename_app_deny_lists.rb: -------------------------------------------------------------------------------- 1 | class RenameAppDenyLists < ActiveRecord::Migration[5.2] 2 | def change 3 | rename_table :app_deny_lists, :deny_lists 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20201110042853_remove_admins_api_key.rb: -------------------------------------------------------------------------------- 1 | class RemoveAdminsApiKey < ActiveRecord::Migration[5.2] 2 | def change 3 | remove_column :admins, :api_key, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/api/apps/webhook.graphql: -------------------------------------------------------------------------------- 1 | query($id: ID!) { 2 | app(id: $id) { 3 | id 4 | webhookKey 5 | webhookUrl 6 | } 7 | viewer { 8 | email 9 | siteAdmin 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/api/deliveries/html.graphql: -------------------------------------------------------------------------------- 1 | query($id: ID!) { 2 | email(id: $id) { 3 | content { 4 | html 5 | } 6 | } 7 | viewer { 8 | email 9 | siteAdmin 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/views/main/reputation.html.haml: -------------------------------------------------------------------------------- 1 | .page-header 2 | %h1 Reputation 3 | 4 | #reputation 5 | %p 6 | %i.fa.fa-spinner.fa-spin.fa-lg 7 | Looking up whether this server is deny listed anywhere... 8 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /bin/delayed_job: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'config', 'environment')) 4 | require 'delayed/command' 5 | Delayed::Command.new(ARGV).daemonize 6 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | load File.expand_path("spring", __dir__) 3 | APP_PATH = File.expand_path('../config/application', __dir__) 4 | require_relative "../config/boot" 5 | require "rails/commands" 6 | -------------------------------------------------------------------------------- /db/migrate/20200902023500_revert_add_key_index_to_meta_values.rb: -------------------------------------------------------------------------------- 1 | class RevertAddKeyIndexToMetaValues < ActiveRecord::Migration[5.2] 2 | def change 3 | remove_index :meta_values, :key 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/filters/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Filters 4 | class Base 5 | # Override this method 6 | def filter_mail(mail) 7 | mail 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /provisioning/requirements.txt: -------------------------------------------------------------------------------- 1 | # Install the correct version of ansible (best to do in a virtual 2 | # environment) with: 3 | # cd provisioning 4 | # pip install -r requirements.txt 5 | 6 | ansible==2.8.17 7 | -------------------------------------------------------------------------------- /app/controllers/landing_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class LandingController < ApplicationController 4 | def index 5 | redirect_to dash_path if session[:jwt_token] 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20130709001223_add_url_index_to_links.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddUrlIndexToLinks < ActiveRecord::Migration[4.2] 4 | def change 5 | add_index :links, :url 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20130407044432_rename_table.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class RenameTable < ActiveRecord::Migration[4.2] 4 | def change 5 | rename_table :to_addresses_emails, :deliveries 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20130414064809_add_index_to_addresses.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddIndexToAddresses < ActiveRecord::Migration[4.2] 4 | def change 5 | add_index :addresses, :text 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20141130220259_add_name_to_admins.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddNameToAdmins < ActiveRecord::Migration[4.2] 4 | def change 5 | add_column :admins, :name, :string 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20200902042840_remove_limit_on_url_length.rb: -------------------------------------------------------------------------------- 1 | class RemoveLimitOnUrlLength < ActiveRecord::Migration[5.2] 2 | def change 3 | change_column :links, :url, :string, limit: nil, null: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/models/acme_challenge_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe AcmeChallenge, type: :model do 6 | pending "add some examples to (or delete) #{__FILE__}" 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20130425025753_add_app_id_to_emails.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddAppIdToEmails < ActiveRecord::Migration[4.2] 4 | def change 5 | add_column :emails, :app_id, :integer 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20130712034237_rename_link_events.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class RenameLinkEvents < ActiveRecord::Migration[4.2] 4 | def change 5 | rename_table :link_events, :click_events 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20141228101710_remove_url_from_apps.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class RemoveUrlFromApps < ActiveRecord::Migration[4.2] 4 | def change 5 | remove_column :apps, :url, :string 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /dockerfiles/mailcatcher: -------------------------------------------------------------------------------- 1 | # Ruby version isn't very important here as this is only used in development 2 | FROM ruby:3.1.3 3 | 4 | RUN gem install mailcatcher 5 | 6 | CMD ["mailcatcher", "--ip", "0.0.0.0", "--foreground"] 7 | -------------------------------------------------------------------------------- /app/graphql/types/dnsbl.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class DNSBL < GraphQL::Schema::Object 5 | field :dnsbl, String, null: false 6 | field :meaning, String, null: false 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/models/deny_list.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class DenyList < ApplicationRecord 4 | belongs_to :app 5 | belongs_to :address 6 | belongs_to :caused_by_postfix_log_line, class_name: "PostfixLogLine" 7 | end 8 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | Rails.application.config.session_store :cookie_store, key: "_cuttlefish_session" 6 | -------------------------------------------------------------------------------- /db/migrate/20130420061331_add_ip_to_open_events.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddIpToOpenEvents < ActiveRecord::Migration[4.2] 4 | def change 5 | add_column :open_events, :ip, :string 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20130617000225_remove_status_from_emails.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class RemoveStatusFromEmails < ActiveRecord::Migration[4.2] 4 | def change 5 | remove_column :emails, :status 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20130710003548_add_subject_to_emails.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddSubjectToEmails < ActiveRecord::Migration[4.2] 4 | def change 5 | add_column :emails, :subject, :string 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/api/apps/index.graphql: -------------------------------------------------------------------------------- 1 | { 2 | apps { 3 | id 4 | name 5 | dkimEnabled 6 | dkimDnsRecord { 7 | upgradeRequired 8 | } 9 | } 10 | viewer { 11 | email 12 | siteAdmin 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /db/migrate/20130407185455_add_message_id_to_emails.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddMessageIdToEmails < ActiveRecord::Migration[4.2] 4 | def change 5 | add_column :emails, :message_id, :string 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20130407192417_add_data_hash_to_emails.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddDataHashToEmails < ActiveRecord::Migration[4.2] 4 | def change 5 | add_column :emails, :data_hash, :string 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20130410050345_add_index_delivered_to_emails.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddIndexDeliveredToEmails < ActiveRecord::Migration[4.2] 4 | def change 5 | add_index :emails, :delivered 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20130424051134_add_index_to_deliveries.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddIndexToDeliveries < ActiveRecord::Migration[4.2] 4 | def change 5 | add_index :deliveries, :open_tracked_hash 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20140713004313_add_from_domain_to_apps.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddFromDomainToApps < ActiveRecord::Migration[4.2] 4 | def change 5 | add_column :apps, :from_domain, :string 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20180904022131_rename_black_lists_table.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class RenameBlackListsTable < ActiveRecord::Migration[5.2] 4 | def change 5 | rename_table :black_lists, :deny_lists 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/api/teams/invite.graphql: -------------------------------------------------------------------------------- 1 | mutation($email: String!, $acceptUrl: String!) { 2 | inviteTeam(email: $email, acceptUrl: $acceptUrl) { 3 | errors { 4 | message 5 | path 6 | type 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /provisioning/roles/cuttlefish-app/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # handlers file for cuttlefish-app 3 | - name: nginx restart 4 | service: name=nginx state=restarted 5 | 6 | - name: nginx reload 7 | service: name=nginx state=reloaded 8 | -------------------------------------------------------------------------------- /db/migrate/20130410043016_add_index_created_at_in_emails.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddIndexCreatedAtInEmails < ActiveRecord::Migration[4.2] 4 | def change 5 | add_index :emails, :created_at 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20130418095518_add_referer_to_open_events.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddRefererToOpenEvents < ActiveRecord::Migration[4.2] 4 | def change 5 | add_column :open_events, :referer, :text 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20141130114112_add_app_id_index_to_deliveries.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddAppIdIndexToDeliveries < ActiveRecord::Migration[4.2] 4 | def change 5 | add_index :deliveries, :app_id 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /db/migrate/20130407053630_rename_address_in_addresses.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class RenameAddressInAddresses < ActiveRecord::Migration[4.2] 4 | def change 5 | rename_column :addresses, :address, :text 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20130420092059_add_combined_index_on_emails.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddCombinedIndexOnEmails < ActiveRecord::Migration[4.2] 4 | def change 5 | add_index :emails, [:created_at, :status] 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/graphql/types/family_and_version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class FamilyAndVersion < GraphQL::Schema::Object 5 | field :family, String, null: true 6 | field :version, String, null: true 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20130410151634_remove_delivered_from_emails.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class RemoveDeliveredFromEmails < ActiveRecord::Migration[4.2] 4 | def change 5 | remove_column :emails, :delivered, :boolean 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20130416043210_add_user_agent_to_open_events.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddUserAgentToOpenEvents < ActiveRecord::Migration[4.2] 4 | def change 5 | add_column :open_events, :user_agent, :string 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20140714022048_remove_dkim_public_key_from_apps.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class RemoveDkimPublicKeyFromApps < ActiveRecord::Migration[4.2] 4 | def change 5 | remove_column :apps, :dkim_public_key 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20141222054155_allow_null_team_id_in_apps.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AllowNullTeamIdInApps < ActiveRecord::Migration[4.2] 4 | def change 5 | change_column :apps, :team_id, :integer, null: true 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20180921001452_rename_super_admin_column.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class RenameSuperAdminColumn < ActiveRecord::Migration[5.2] 4 | def change 5 | rename_column :admins, :super_admin, :site_admin 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/api/invitations/create.graphql: -------------------------------------------------------------------------------- 1 | mutation($email: String!, $acceptUrl: String!) { 2 | inviteAdminToTeam(email: $email, acceptUrl: $acceptUrl) { 3 | errors { 4 | message 5 | path 6 | type 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /spec/factories/links.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Read about factories at https://github.com/thoughtbot/factory_girl 4 | 5 | FactoryBot.define do 6 | factory :link do 7 | url { "http://foo.com" } 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: test 6 | 7 | production: 8 | adapter: redis 9 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> 10 | channel_prefix: cuttlefish_production 11 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /db/migrate/20130405045356_add_postfix_queue_id_to_emails.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddPostfixQueueIdToEmails < ActiveRecord::Migration[4.2] 4 | def change 5 | add_column :emails, :postfix_queue_id, :string 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20130405081503_add_time_to_postfix_log_lines.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddTimeToPostfixLogLines < ActiveRecord::Migration[4.2] 4 | def change 5 | add_column :postfix_log_lines, :time, :datetime 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20130410083333_add_index_delivery_status_to_emails.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddIndexDeliveryStatusToEmails < ActiveRecord::Migration[4.2] 4 | def change 5 | add_index :emails, :delivery_status 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20130429005659_add_deliver_id_index_to_open_events.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddDeliverIdIndexToOpenEvents < ActiveRecord::Migration[4.2] 4 | def change 5 | add_index :open_events, :delivery_id 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20130607022711_rename_cuttlefish_field_in_apps.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class RenameCuttlefishFieldInApps < ActiveRecord::Migration[4.2] 4 | def change 5 | rename_column :apps, :cuttlefish, :default_app 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20141126052536_create_teams.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateTeams < ActiveRecord::Migration[4.2] 4 | def change 5 | create_table :teams do |t| 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20200913012801_remove_caused_by_delivery_id_from_deny_lists.rb: -------------------------------------------------------------------------------- 1 | class RemoveCausedByDeliveryIdFromDenyLists < ActiveRecord::Migration[5.2] 2 | def change 3 | remove_column :deny_lists, :caused_by_delivery_id, :bigint 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20240624092819_add_disable_css_inlining_to_emails.rb: -------------------------------------------------------------------------------- 1 | class AddDisableCssInliningToEmails < ActiveRecord::Migration[6.1] 2 | def change 3 | add_column :emails, :disable_css_inlining, :boolean, null: false, default: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/api/passwords/create.graphql: -------------------------------------------------------------------------------- 1 | mutation($email: String!, $resetUrl: String!) { 2 | sendResetPasswordInstructions(email: $email, resetUrl: $resetUrl) { 3 | errors { 4 | message 5 | path 6 | type 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /db/migrate/20130406061755_remove_not_delivered_from_emails.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class RemoveNotDeliveredFromEmails < ActiveRecord::Migration[4.2] 4 | def change 5 | remove_column :emails, :not_delivered, :boolean 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20130410160315_rename_delivery_status_in_emails.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class RenameDeliveryStatusInEmails < ActiveRecord::Migration[4.2] 4 | def change 5 | rename_column :emails, :delivery_status, :status 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20130425050658_add_cuttlefish_to_apps.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddCuttlefishToApps < ActiveRecord::Migration[4.2] 4 | def change 5 | add_column :apps, :cuttlefish, :boolean, null: false, default: false 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20130430003245_add_another_index_to_deliveries.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddAnotherIndexToDeliveries < ActiveRecord::Migration[4.2] 4 | def change 5 | add_index :deliveries, [:open_tracked, :created_at] 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20240624093627_remove_default_on_disable_css_inlining.rb: -------------------------------------------------------------------------------- 1 | class RemoveDefaultOnDisableCssInlining < ActiveRecord::Migration[6.1] 2 | def change 3 | change_column_default :emails, :disable_css_inlining, from: false, to: nil 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/api/apps/edit.graphql: -------------------------------------------------------------------------------- 1 | query($id: ID!) { 2 | app(id: $id) { 3 | id 4 | name 5 | clickTrackingEnabled 6 | openTrackingEnabled 7 | customTrackingDomain 8 | } 9 | viewer { 10 | email 11 | siteAdmin 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/api/passwords/update.graphql: -------------------------------------------------------------------------------- 1 | mutation($token: String!, $password: String!) { 2 | resetPasswordByToken(token: $token, password: $password) { 3 | token 4 | errors { 5 | message 6 | path 7 | type 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /provisioning/roles/cuttlefish-app/templates/database.yml: -------------------------------------------------------------------------------- 1 | production: 2 | adapter: postgresql 3 | database: cuttlefish 4 | username: cuttlefish 5 | password: {{ db_password }} 6 | # Because we're using sidekiq for the background queue 7 | pool: 25 8 | -------------------------------------------------------------------------------- /spec/factories/admins.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :admin do 5 | sequence :email do |n| 6 | "person#{n}@foo.com" 7 | end 8 | password { "password" } 9 | team 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20130427055637_add_index_to_deliveries_join_table.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddIndexToDeliveriesJoinTable < ActiveRecord::Migration[4.2] 4 | def change 5 | add_index :deliveries, [:email_id, :address_id] 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20130618021807_add_address_id_index_to_deliveries.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddAddressIdIndexToDeliveries < ActiveRecord::Migration[4.2] 4 | def change 5 | add_index :deliveries, [:address_id, :created_at] 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20140714004505_add_dkim_enabled_to_apps.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddDkimEnabledToApps < ActiveRecord::Migration[4.2] 4 | def change 5 | add_column :apps, :dkim_enabled, :boolean, null: false, default: false 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/api/documentation/index.graphql: -------------------------------------------------------------------------------- 1 | { 2 | apps { 3 | id 4 | name 5 | smtpServer { 6 | hostname 7 | port 8 | username 9 | password 10 | } 11 | } 12 | viewer { 13 | email 14 | siteAdmin 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/views/documentation/_other.html.haml: -------------------------------------------------------------------------------- 1 | %h3 Using other frameworks 2 | 3 | %p Want to add instructions here for your favorite web framework and make it super easy for people? Please #{link_to "contribute", "https://github.com/mlandauer/cuttlefish#how-to-contribute"}. 4 | -------------------------------------------------------------------------------- /db/migrate/20130410000152_add_indexes_to_tables.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddIndexesToTables < ActiveRecord::Migration[4.2] 4 | def change 5 | add_index :emails, :postfix_queue_id 6 | add_index :postfix_log_lines, :time 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20130416050249_add_open_tracked_hash_to_deliveries.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddOpenTrackedHashToDeliveries < ActiveRecord::Migration[4.2] 4 | def change 5 | add_column :deliveries, :open_tracked_hash, :string 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20130430053701_add_composite_index_to_deliveries.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddCompositeIndexToDeliveries < ActiveRecord::Migration[4.2] 4 | def change 5 | add_index :deliveries, [:created_at, :open_events_count] 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20141201051257_add_super_admin_to_admins.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddSuperAdminToAdmins < ActiveRecord::Migration[4.2] 4 | def change 5 | add_column :admins, :super_admin, :boolean, null: false, default: false 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/api/admins/index.graphql: -------------------------------------------------------------------------------- 1 | { 2 | admins { 3 | id 4 | name 5 | email 6 | displayName 7 | invitationCreatedAt 8 | invitationAcceptedAt 9 | currentAdmin 10 | } 11 | viewer { 12 | email 13 | siteAdmin 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/api/apps/create.graphql: -------------------------------------------------------------------------------- 1 | mutation($attributes: AppAttributes!) { 2 | createApp(attributes: $attributes) { 3 | app { 4 | id 5 | name 6 | } 7 | errors { 8 | message 9 | path 10 | type 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/assets/javascripts/documentation.js: -------------------------------------------------------------------------------- 1 | // If an anchor is used forces the corresponding tab to be opened on loading of the page 2 | $(document).ready(function() { 3 | if (location.hash !== '') { 4 | $(`a[href="${location.hash}"]`).tab('show'); 5 | } 6 | }); 7 | -------------------------------------------------------------------------------- /app/graphql/types/open_event.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class OpenEvent < GraphQL::Schema::Object 5 | implements Types::UserAgentEvent 6 | 7 | description "Information about someone opening an email" 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/helpers/admins_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AdminsHelper 4 | def email_with_name(admin) 5 | if admin.name.present? 6 | "#{admin.name} <#{admin.email}>" 7 | else 8 | admin.email 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/models/click_event.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ClickEvent < ApplicationRecord 4 | belongs_to :delivery_link, counter_cache: true 5 | delegate :link, to: :delivery_link 6 | include UserAgent 7 | 8 | delegate :url, to: :link 9 | end 10 | -------------------------------------------------------------------------------- /app/models/link.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Link < ApplicationRecord 4 | has_many :click_events, through: :delivery_links 5 | has_many :delivery_links, dependent: :restrict_with_exception 6 | has_many :deliveries, through: :delivery_links 7 | end 8 | -------------------------------------------------------------------------------- /app/policies/test_email_policy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class TestEmailPolicy < ApplicationPolicy 4 | def new? 5 | create? 6 | end 7 | 8 | def create? 9 | user && !Rails.configuration.cuttlefish_read_only_mode 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20130406074059_run_update_deliver_status.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class RunUpdateDeliverStatus < ActiveRecord::Migration[4.2] 4 | def up 5 | Email.all.each do |email| 6 | email.update_status! 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20130410032907_remove_email_id_from_postfix_log_lines.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class RemoveEmailIdFromPostfixLogLines < ActiveRecord::Migration[4.2] 4 | def change 5 | remove_column :postfix_log_lines, :email_id, :integer 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20130524114642_remove_open_tracked_hash_from_deliveries.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class RemoveOpenTrackedHashFromDeliveries < ActiveRecord::Migration[4.2] 4 | def change 5 | remove_column :deliveries, :open_tracked_hash 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20130712062539_rename_link_tracking_enabled.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class RenameLinkTrackingEnabled < ActiveRecord::Migration[4.2] 4 | def change 5 | rename_column :apps, :link_tracking_enabled, :click_tracking_enabled 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20141118012335_remove_default_app_from_apps.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class RemoveDefaultAppFromApps < ActiveRecord::Migration[4.2] 4 | def change 5 | remove_column :apps, :default_app, :boolean, default: false, null: false 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20141222053542_add_cuttlefish_to_apps_again.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddCuttlefishToAppsAgain < ActiveRecord::Migration[4.2] 4 | def change 5 | add_column :apps, :cuttlefish, :boolean, null: false, default: false 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20181011045125_remove_smtp_password_locked_field_from_app.rb: -------------------------------------------------------------------------------- 1 | class RemoveSmtpPasswordLockedFieldFromApp < ActiveRecord::Migration[5.2] 2 | def change 3 | remove_column :apps, :smtp_password_locked, :boolean, default: false, null: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/controllers/acme_challenges_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AcmeChallengesController < ApplicationController 4 | def show 5 | challenge = AcmeChallenge.find_by!(token: params[:token]) 6 | render plain: challenge.content 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20130425022022_add_password_locked_to_apps.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddPasswordLockedToApps < ActiveRecord::Migration[4.2] 4 | def change 5 | add_column :apps, :smtp_password_locked, :boolean, null: false, default: false 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20130430032442_add_composite_index_to_postfix_log_lines.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddCompositeIndexToPostfixLogLines < ActiveRecord::Migration[4.2] 4 | def change 5 | add_index :postfix_log_lines, [:delivery_id, :time] 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/api/invitations/create_error.graphql: -------------------------------------------------------------------------------- 1 | { 2 | admins { 3 | id 4 | name 5 | email 6 | displayName 7 | invitationCreatedAt 8 | invitationAcceptedAt 9 | currentAdmin 10 | } 11 | viewer { 12 | email 13 | siteAdmin 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Procfile.production: -------------------------------------------------------------------------------- 1 | smtp: /usr/local/lib/rvm/bin/rvm . do bundle exec rake cuttlefish:smtp RAILS_ENV=production 2 | log: /usr/local/lib/rvm/bin/rvm . do bundle exec rake cuttlefish:log RAILS_ENV=production 3 | sidekiq: /usr/local/lib/rvm/bin/rvm . do bundle exec sidekiq -e production 4 | -------------------------------------------------------------------------------- /db/migrate/20130412070256_change_default_status_of_emails.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ChangeDefaultStatusOfEmails < ActiveRecord::Migration[4.2] 4 | def change 5 | change_column :emails, :status, :string, null: false, default: "not_sent" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20130416012515_add_open_tracked_to_deliveries.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddOpenTrackedToDeliveries < ActiveRecord::Migration[4.2] 4 | def change 5 | add_column :deliveries, :open_tracked, :boolean, null: false, default: false 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20130610071554_fix_foreign_keys.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class FixForeignKeys < ActiveRecord::Migration[4.2] 4 | def change 5 | Delivery.find_each do |delivery| 6 | delivery.destroy if delivery.email.nil? 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/api/apps/update.graphql: -------------------------------------------------------------------------------- 1 | mutation($id: ID!, $attributes: AppAttributes!) { 2 | updateApp(id: $id, attributes: $attributes) { 3 | app { 4 | id 5 | name 6 | } 7 | errors { 8 | message 9 | path 10 | type 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /db/migrate/20130406040719_add_delivered_to_emails.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddDeliveredToEmails < ActiveRecord::Migration[4.2] 4 | def change 5 | add_column :emails, :delivered, :boolean 6 | add_column :emails, :not_delivered, :boolean 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20130425023811_rename_name_fields_in_apps.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class RenameNameFieldsInApps < ActiveRecord::Migration[4.2] 4 | def change 5 | rename_column :apps, :name, :smtp_username 6 | rename_column :apps, :description, :name 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20130602040058_add_open_tracking_enabled_to_apps.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddOpenTrackingEnabledToApps < ActiveRecord::Migration[4.2] 4 | def change 5 | add_column :apps, :open_tracking_enabled, :boolean, null: false, default: true 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20130605065600_create_links.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateLinks < ActiveRecord::Migration[4.2] 4 | def change 5 | create_table :links do |t| 6 | t.string :url, null: false 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20140713001257_add_dkim_key_pair_to_apps.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddDkimKeyPairToApps < ActiveRecord::Migration[4.2] 4 | def change 5 | add_column :apps, :dkim_public_key, :text 6 | add_column :apps, :dkim_private_key, :text 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20220606034659_add_custom_tracking_domain_ssl_enabled_to_apps.rb: -------------------------------------------------------------------------------- 1 | class AddCustomTrackingDomainSslEnabledToApps < ActiveRecord::Migration[5.2] 2 | def change 3 | add_column :apps, :custom_tracking_domain_ssl_enabled, :boolean, null: false, default: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/api/registrations/create.graphql: -------------------------------------------------------------------------------- 1 | mutation($name: String, $email: String!, $password: String!) { 2 | registerSiteAdmin(name: $name, email: $email, password: $password) { 3 | token 4 | errors { 5 | path 6 | type 7 | message 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /provisioning/roles/cuttlefish-postfix/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # handlers file for postfix 3 | - name: restart postfix 4 | service: 5 | name: postfix 6 | state: restarted 7 | 8 | - name: restart rsyslog 9 | service: 10 | name: rsyslog 11 | state: restarted 12 | -------------------------------------------------------------------------------- /app/views/main/index.html.haml: -------------------------------------------------------------------------------- 1 | .page-header 2 | %h1 Dashboard 3 | %p Summary of emails sent and received today and in the last week. 4 | 5 | %p= link_to "How to send email via Cuttlefish", documentation_path 6 | 7 | .row#status-counts 8 | = render "main/status_counts", loading: true 9 | -------------------------------------------------------------------------------- /lib/api/invitations/update.graphql: -------------------------------------------------------------------------------- 1 | mutation($name: String, $password: String!, $token: String!) { 2 | acceptAdminInvitation(name: $name, password: $password, token: $token) { 3 | token 4 | errors { 5 | message 6 | path 7 | type 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/api/sessions/create.graphql: -------------------------------------------------------------------------------- 1 | mutation($email: String!, $password: String!) { 2 | loginAdmin(email: $email, password: $password) { 3 | token 4 | admin { 5 | id 6 | } 7 | errors { 8 | message 9 | path 10 | type 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /spec/factories/delivery_links.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Read about factories at https://github.com/thoughtbot/factory_girl 4 | 5 | FactoryBot.define do 6 | factory :delivery_link do 7 | delivery 8 | link 9 | click_events { [] } 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/controllers/documentation_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class DocumentationController < ApplicationController 4 | def index 5 | result = api_query 6 | @data = result.data 7 | @apps = @data.apps 8 | @active_app = @apps.first 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/helpers/deny_lists_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DenyListsHelper 4 | def canonical_deny_lists_path(app:, dsn:) 5 | if app 6 | app_deny_lists_path(app.id, dsn: dsn) 7 | else 8 | deny_lists_path(dsn: dsn) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/views/devise/mailer/confirmation_instructions.html.erb: -------------------------------------------------------------------------------- 1 |
Welcome <%= @resource.email %>!
2 | 3 |You can confirm your account through the link below:
4 | 5 |<%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @resource.confirmation_token) %>
6 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ActiveSupport::Reloader.to_prepare do 4 | # ApplicationController.renderer.defaults.merge!( 5 | # http_host: 'example.org', 6 | # https: false 7 | # ) 8 | # end 9 | -------------------------------------------------------------------------------- /db/migrate/20130330230840_add_email_address_id_to_email.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddEmailAddressIdToEmail < ActiveRecord::Migration[4.2] 4 | def change 5 | add_column :emails, :from_address_id, :integer 6 | remove_column :emails, :from, :string 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20141124001230_add_archived_deliveries_count_to_apps.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddArchivedDeliveriesCountToApps < ActiveRecord::Migration[4.2] 4 | def change 5 | add_column :apps, :archived_deliveries_count, :integer, null: false, default: 0 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Specify a serializer for the signed and encrypted cookie jars. 4 | # Valid options are :json, :marshal, and :hybrid. 5 | Rails.application.config.action_dispatch.cookies_serializer = :marshal 6 | -------------------------------------------------------------------------------- /db/migrate/20130330193042_create_emails.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateEmails < ActiveRecord::Migration[4.2] 4 | def change 5 | create_table :emails do |t| 6 | t.string :from 7 | t.string :to 8 | 9 | t.timestamps 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20130416010949_create_open_events.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateOpenEvents < ActiveRecord::Migration[4.2] 4 | def change 5 | create_table :open_events do |t| 6 | t.integer :delivery_id 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/graphql/types/key_value.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class KeyValue < GraphQL::Schema::Object 5 | description "A key/value pair" 6 | field :key, String, 7 | null: false 8 | field :value, String, 9 | null: false 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/views/main/_status_counts.html.haml: -------------------------------------------------------------------------------- 1 | .span3 2 | %h3 Last 24 hours 3 | = render "main/status_counts_period", stats: @stats_today, link: true, loading: loading 4 | 5 | .span3.offset3 6 | %h3 Last 7 days 7 | = render "main/status_counts_period", stats: @stats_this_week, link: true, loading: loading 8 | -------------------------------------------------------------------------------- /db/migrate/20130330222744_create_email_addresses.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateEmailAddresses < ActiveRecord::Migration[4.2] 4 | def change 5 | create_table :email_addresses do |t| 6 | t.string :address 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20130410152558_add_default_to_delivery_status_in_emails.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddDefaultToDeliveryStatusInEmails < ActiveRecord::Migration[4.2] 4 | def change 5 | change_column :emails, :delivery_status, :string, null: false, default: "unknown" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20141107045312_fix_invitation_for_admins.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class FixInvitationForAdmins < ActiveRecord::Migration[4.2] 4 | def change 5 | add_column :admins, :invitation_created_at, :datetime 6 | change_column :admins, :invitation_token, :string 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/api/teams/index.graphql: -------------------------------------------------------------------------------- 1 | { 2 | teams { 3 | admins { 4 | email 5 | displayName 6 | } 7 | apps { 8 | id 9 | name 10 | } 11 | } 12 | cuttlefishApp { 13 | id 14 | name 15 | } 16 | viewer { 17 | email 18 | siteAdmin 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /db/migrate/20130407045750_rename_table_email_addresses.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class RenameTableEmailAddresses < ActiveRecord::Migration[4.2] 4 | def change 5 | rename_table :email_addresses, :addresses 6 | rename_column :deliveries, :email_address_id, :address_id 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20220601230818_create_acme_challenges.rb: -------------------------------------------------------------------------------- 1 | class CreateAcmeChallenges < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :acme_challenges do |t| 4 | t.string :token, index: { unique: true } 5 | t.string :content 6 | t.timestamps 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/factories/deny_lists.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Read about factories at https://github.com/thoughtbot/factory_girl 4 | 5 | FactoryBot.define do 6 | factory :deny_list do 7 | app 8 | address 9 | caused_by_postfix_log_line factory: :postfix_log_line 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /config/initializers/graphiql.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | if Rails.env.development? 4 | GraphiQL::Rails.config.headers["Authorization"] = lambda do |context| 5 | # GraphiQL will make requests on behalf of the currently logged in admin 6 | "Bearer #{context.session[:jwt_token]}" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20200812050040_add_ignore_deny_list_to_emails.rb: -------------------------------------------------------------------------------- 1 | class AddIgnoreDenyListToEmails < ActiveRecord::Migration[5.2] 2 | def change 3 | add_column :emails, :ignore_deny_list, :boolean, null: false, default: false 4 | change_column_default :emails, :ignore_deny_list, from: false, to: nil 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/api/clients/index.graphql: -------------------------------------------------------------------------------- 1 | query($appId: ID) { 2 | emails(appId: $appId) { 3 | statistics { 4 | userAgentFamilyCounts { 5 | name 6 | count 7 | } 8 | } 9 | } 10 | apps { 11 | id 12 | name 13 | } 14 | viewer { 15 | email 16 | siteAdmin 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/api/registrations/update.graphql: -------------------------------------------------------------------------------- 1 | mutation($email: String!, $name: String!, $password: String, $currentPassword: String!) { 2 | updateAdmin(email: $email, name: $name, password: $password, currentPassword: $currentPassword) { 3 | errors { 4 | path 5 | type 6 | message 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Add your own tasks in files placed in lib/tasks ending in .rake, 4 | # for example lib/tasks/capistrano.rake, and they will automatically be 5 | # available to Rake. 6 | 7 | require File.expand_path("config/application", __dir__) 8 | 9 | Cuttlefish::Application.load_tasks 10 | -------------------------------------------------------------------------------- /app/assets/stylesheets/coderay.css.scss: -------------------------------------------------------------------------------- 1 | .CodeRay .comment { color:#E67E22; font-style: italic; } /* comment */ 2 | .CodeRay .integer { color:#27AE60 } /* integer */ 3 | .CodeRay .string { color:#27AE60 } /* string */ 4 | .CodeRay .symbol { color:#2980B9 } /* symbol */ 5 | -------------------------------------------------------------------------------- /config/initializers/app_version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | unless defined?(APP_VERSION) 4 | APP_VERSION = if Rails.env.production? 5 | File.read(File.join(Rails.root, "REVISION"))[0..6] 6 | else 7 | `git describe --always` 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20141130113657_update_app_id_in_deliveries.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UpdateAppIdInDeliveries < ActiveRecord::Migration[4.2] 4 | def change 5 | #Delivery.connection.execute('UPDATE deliveries JOIN emails ON deliveries.email_id = emails.id SET deliveries.app_id = emails.app_id') 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/views/devise/shared/_links.html.erb: -------------------------------------------------------------------------------- 1 | <%- if controller_name != 'sessions' %> 2 | <%= link_to "Sign in", new_admin_session_path, class: "login-link" %>Hello <%= @resource.email %>!
2 | 3 |Your account has been locked due to an excessive amount of unsuccessful sign in attempts.
4 | 5 |Click the link below to unlock your account:
6 | 7 |<%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @resource.unlock_token) %>
8 | -------------------------------------------------------------------------------- /db/migrate/20130602070927_add_link_tracking_enabled_to_apps.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddLinkTrackingEnabledToApps < ActiveRecord::Migration[4.2] 4 | def change 5 | add_column :apps, :link_tracking_enabled, :boolean, null: false, default: true 6 | rename_column :apps, :open_tracking_domain, :custom_tracking_domain 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20130605070327_create_delivery_links.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateDeliveryLinks < ActiveRecord::Migration[4.2] 4 | def change 5 | create_table :delivery_links do |t| 6 | t.integer :delivery_id, null: false 7 | t.integer :link_id, null: false 8 | 9 | t.timestamps 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20140702031017_change_handler_to_medium_text_on_delayed_jobs.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ChangeHandlerToMediumTextOnDelayedJobs < ActiveRecord::Migration[4.2] 4 | def change 5 | if ActiveRecord::Base.connection.adapter_name == 'Mysql2' 6 | change_column :delayed_jobs, :handler, :mediumtext 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/setup_custom_tracking_domain_ssl_worker.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # For Sidekiq 4 | class SetupCustomTrackingDomainSSLWorker 5 | include Sidekiq::Worker 6 | 7 | def perform(app_id) 8 | app = App.find(app_id) 9 | AppServices::SetupCustomTrackingDomainSSL.call(app: app) 10 | # TODO: Check whether that worked 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20130410082419_add_delivery_status_to_emails.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddDeliveryStatusToEmails < ActiveRecord::Migration[4.2] 4 | def change 5 | add_column :emails, :delivery_status, :string 6 | Email.reset_column_information 7 | Email.all.each do |email| 8 | email.update_status! 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20130410153952_cleanup_postfix_log_lines.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CleanupPostfixLogLines < ActiveRecord::Migration[4.2] 4 | def change 5 | remove_column :postfix_log_lines, :text, :text 6 | remove_column :postfix_log_lines, :to, :string 7 | rename_column :postfix_log_lines, :status, :extended_status 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/api/domains/index.graphql: -------------------------------------------------------------------------------- 1 | query ($since: DateTime!) { 2 | emails(since: $since) { 3 | statistics { 4 | hardBounceCountByToDomain { 5 | count 6 | name 7 | } 8 | deliveredCountByToDomain { 9 | count 10 | name 11 | } 12 | } 13 | } 14 | viewer { 15 | email 16 | siteAdmin 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/graphql/types/blocked_address_permissions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class BlockedAddressPermissions < GraphQL::Schema::Object 5 | description "Permissions for current admin for accessing and editing " \ 6 | "a blocked address" 7 | 8 | field :destroy, Boolean, null: false, method: :destroy? 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20130405184644_convert_string_to_text_in_postfix_log_lines.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ConvertStringToTextInPostfixLogLines < ActiveRecord::Migration[4.2] 4 | def up 5 | change_column :postfix_log_lines, :text, :text, limit: nil 6 | end 7 | 8 | def down 9 | change_column :postfix_log_lines, :text, :string 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/api/test_emails/create.graphql: -------------------------------------------------------------------------------- 1 | mutation ($appId: ID!, $from: String!, $to: [String!]!, $subject: String!, $textPart: String, $htmlPart: String, $metaValues: [KeyValueAttributes!]) { 2 | createEmails(appId: $appId, from: $from, to: $to, subject: $subject, textPart: $textPart, htmlPart: $htmlPart, metaValues: $metaValues) { 3 | emails { 4 | id 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lib/post_delivery_event_worker.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # For Sidekiq 4 | class PostDeliveryEventWorker 5 | include Sidekiq::Worker 6 | 7 | def perform(url, key, id) 8 | event = PostfixLogLine.find(id) 9 | WebhookServices::PostDeliveryEvent.call( 10 | url: url, 11 | key: key, 12 | event: event 13 | ) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/migrate/20130430235232_tweak_postfix_log_lines_index_order.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class TweakPostfixLogLinesIndexOrder < ActiveRecord::Migration[4.2] 4 | def change 5 | add_index :postfix_log_lines, [:time, :delivery_id] 6 | remove_index :postfix_log_lines, [:delivery_id, :time] 7 | remove_index :postfix_log_lines, :time 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20130606060336_create_link_events.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateLinkEvents < ActiveRecord::Migration[4.2] 4 | def change 5 | create_table :link_events do |t| 6 | t.integer :delivery_link_id 7 | t.text :user_agent 8 | t.text :referer 9 | t.string :ip 10 | 11 | t.timestamps 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/lib/filters/base_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | describe Filters::Base do 6 | let(:mail) do 7 | Mail.new do 8 | body "Some content" 9 | end 10 | end 11 | let(:filter) { described_class.new } 12 | 13 | describe "#filter" do 14 | it { expect(filter.filter_mail(mail)).to eq mail } 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/forms/admin_form.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AdminForm 4 | include ActiveModel::Model 5 | include Virtus.model 6 | 7 | attribute :email, String 8 | attribute :password, String 9 | attribute :name, String 10 | attribute :invitation_token, String 11 | attribute :reset_password_token, String 12 | attribute :current_password, String 13 | end 14 | -------------------------------------------------------------------------------- /spec/factories/postfix_log_lines.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :postfix_log_line do 5 | delivery 6 | 7 | time { Time.zone.now } 8 | relay { "foo.com[1.2.3.4]:25" } 9 | delay { "2.1" } 10 | delays { "0.09/0.02/0.99/0.99" } 11 | dsn { "2.0.0" } 12 | extended_status { "sent (250 ok dirdel)" } 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/graphql/types/key_value_attributes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class KeyValueAttributes < GraphQL::Schema::InputObject 5 | description "Attributes of key, value pairs" 6 | argument :key, 7 | String, 8 | required: true 9 | argument :value, 10 | String, 11 | required: true 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20200818194827_create_meta_values.rb: -------------------------------------------------------------------------------- 1 | class CreateMetaValues < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :meta_values do |t| 4 | t.references :email, null: false, foreign_key: true 5 | t.string :key, null: false 6 | t.string :value, null: false 7 | end 8 | add_index :meta_values, [:email_id, :key], unique: true 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/views/deliveries/_postfix_log_lines.html.haml: -------------------------------------------------------------------------------- 1 | %table.table.table-condensed 2 | %thead 3 | %tr 4 | %th Time 5 | %th DSN 6 | %th Status 7 | %tbody 8 | - delivery.delivery_events.each do |event| 9 | %tr 10 | %td 11 | = time_ago_in_words(event.time) 12 | ago 13 | %td= event.dsn 14 | %td.status= event.extended_status 15 | -------------------------------------------------------------------------------- /db/migrate/20130401040355_add_to_association_to_emails.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddToAssociationToEmails < ActiveRecord::Migration[4.2] 4 | def change 5 | remove_column :emails, :to, :string 6 | create_table :to_addresses_emails do |t| 7 | t.references :email 8 | t.references :email_address 9 | 10 | t.timestamps 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20130424060803_create_apps.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateApps < ActiveRecord::Migration[4.2] 4 | def change 5 | create_table :apps do |t| 6 | t.string :name 7 | t.string :description 8 | t.string :url 9 | t.string :smtp_password 10 | t.string :open_tracking_domain 11 | 12 | t.timestamps 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/filters/mailer_header.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Filters 4 | class MailerHeader < Filters::Base 5 | attr_accessor :version 6 | 7 | def initialize(version:) 8 | super() 9 | @version = version 10 | end 11 | 12 | def filter_mail(mail) 13 | mail.header["X-Mailer"] = "Cuttlefish #{version}" 14 | mail 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/api/apps/show.graphql: -------------------------------------------------------------------------------- 1 | query($id: ID!) { 2 | app(id: $id) { 3 | id 4 | name 5 | cuttlefish 6 | dkimEnabled 7 | dkimDnsRecord { 8 | upgradeRequired 9 | } 10 | customTrackingDomain 11 | customTrackingDomainSslEnabled 12 | permissions { 13 | update 14 | dkim 15 | } 16 | } 17 | viewer { 18 | email 19 | siteAdmin 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/hash_id.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module HashId 4 | def self.hash(message) 5 | # TODO: Rename configuration - it's not a salt, it's a key 6 | OpenSSL::HMAC.hexdigest( 7 | "sha1", 8 | Rails.configuration.cuttlefish_hash_salt, 9 | message 10 | ) 11 | end 12 | 13 | def self.valid?(message, given_hash) 14 | hash(message) == given_hash 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/controllers/clients_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ClientsController < ApplicationController 4 | def index 5 | result = api_query app_id: params[:app_id] 6 | @data = result.data 7 | @client_counts = @data.emails.statistics.user_agent_family_counts 8 | @apps = @data.apps 9 | @app = @apps.find { |a| a.id == params[:app_id] } if params[:app_id] 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/helpers/apps_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AppsHelper 4 | # If a DNS TXT record is longer than 255 characters it needs to be split 5 | # into several separate strings. Some DNS hosting services (e.g. DNS Made 6 | # Easy) expect strings to be formatted in this way. 7 | def quote_long_dns_txt_record(text) 8 | text.scan(/.{1,255}/).map { |s| "\"#{s}\"" }.join 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20141128055957_add_app_id_to_deliveries.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddAppIdToDeliveries < ActiveRecord::Migration[4.2] 4 | def change 5 | add_column :deliveries, :app_id, :integer 6 | #add_index :deliveries, :app_id 7 | #Delivery.connection.execute('UPDATE deliveries JOIN emails ON deliveries.email_id = emails.id SET deliveries.app_id = emails.app_id') 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/factories/emails.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :email do 5 | # The from_address is populated automatically on save from the "data" field 6 | # Setting it here to just make things a little easier in some testing 7 | from_address factory: :address 8 | app 9 | ignore_deny_list { false } 10 | disable_css_inlining { false } 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/helpers/main_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module MainHelper 4 | # Similar to link_to_if but when passed a block behave more like the 5 | # regular link_to 6 | def link_to_if_block(condition, name, options = {}, html_options = {}, &block) 7 | if condition 8 | link_to(name, options, html_options, &block) 9 | else 10 | capture(name, &block) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/reputation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Reputation 4 | def self.local_ip 5 | # turn off reverse DNS resolution temporarily 6 | orig = Socket.do_not_reverse_lookup 7 | Socket.do_not_reverse_lookup = true 8 | 9 | UDPSocket.open do |s| 10 | s.connect "64.233.187.99", 1 11 | s.addr.last 12 | end 13 | ensure 14 | Socket.do_not_reverse_lookup = orig 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /provisioning/requirements.yml: -------------------------------------------------------------------------------- 1 | # Install required roles with 2 | # cd provisioning 3 | # ansible-galaxy install -r requirements.yml -p roles 4 | 5 | - src: newrelic.newrelic-infra 6 | version: 0.8.2 7 | 8 | - src: rvm.ruby 9 | version: v2.1.0 10 | 11 | - src: geerlingguy.certbot 12 | version: 3.0.0 13 | 14 | - src: DavidWittman.redis 15 | version: 1.2.8 16 | 17 | - src: nickhammond.logrotate 18 | version: v0.0.5 19 | -------------------------------------------------------------------------------- /app/views/addresses/from.html.haml: -------------------------------------------------------------------------------- 1 | .page-header 2 | %h1 3 | Emails sent from 4 | = @from 5 | 6 | .row 7 | .span3 8 | %p 9 | = link_to to_address_path(@from) do 10 | Switch to emails sent 11 | %strong to 12 | this address 13 | = render "main/status_counts_period", stats: @stats, link: false, loading: false 14 | = render partial: "deliveries/to", locals: {deliveries: @deliveries} 15 | -------------------------------------------------------------------------------- /db/migrate/20200903034601_add_app_deny_lists.rb: -------------------------------------------------------------------------------- 1 | class AddAppDenyLists < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :app_deny_lists do |t| 4 | t.references :address, null: false, foreign_key: true 5 | t.references :caused_by_delivery, null: false, foreign_key: {to_table: :deliveries} 6 | t.references :app, null: false, foreign_key: true 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20200912053221_drop_deny_lists.rb: -------------------------------------------------------------------------------- 1 | class DropDenyLists < ActiveRecord::Migration[5.2] 2 | def change 3 | drop_table "deny_lists" do |t| 4 | t.integer "address_id" 5 | t.integer "caused_by_delivery_id" 6 | t.datetime "created_at" 7 | t.datetime "updated_at" 8 | t.integer "team_id", null: false 9 | t.index ["team_id"], name: "index_deny_lists_on_team_id" 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/filters/inline_css.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Filters 4 | class InlineCss < Filters::Mail 5 | attr_accessor :enabled 6 | 7 | def initialize(enabled:) 8 | super() 9 | @enabled = enabled 10 | end 11 | 12 | def filter_html(input) 13 | if enabled 14 | TransformHtml.new(input).inline_css 15 | else 16 | input 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/views/devise/unlocks/new.html.erb: -------------------------------------------------------------------------------- 1 |Hello <%= @resource.email %>!
2 | 3 |Someone has requested a link to change your password. You can do this through the link below.
4 | 5 |<%= link_to 'Change my password', "#{@reset_url}?#{ {reset_password_token: @token}.to_query }" %>
6 | 7 |If you didn't request this, please ignore this email.
8 |Your password won't change until you access the link above and create a new one.
9 | -------------------------------------------------------------------------------- /db/migrate/20130616063653_add_status_to_deliveries.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddStatusToDeliveries < ActiveRecord::Migration[4.2] 4 | def change 5 | add_column :deliveries, :status, :string, null: false 6 | Delivery.reset_column_information 7 | Delivery.find_each do |delivery| 8 | # Activerecord callbacks will not be called 9 | delivery.update_columns(status: delivery.calculated_status) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/policies/meta_value_policy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class MetaValuePolicy < ApplicationPolicy 4 | class Scope < Scope 5 | def resolve 6 | # Avoid using join here as it was a lot slower 7 | app_ids = AppPolicy::Scope.new(user, App).resolve.pluck(:id) 8 | # But we're not completely avoiding joins here, so it will probably be quite slow 9 | scope.joins(:email).where(emails: { app_id: app_ids }) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/views/devise/mailer/invitation_instructions.html.erb: -------------------------------------------------------------------------------- 1 |<%= @resource.invited_by.display_name %> invites you to Cuttlefish - an easy to use transactional email server with a lovely user interface
2 | 3 |Accept the invitation through the link below.
4 | 5 |<%= link_to t("devise.mailer.invitation_instructions.accept"), "#{@accept_url}?#{ {invitation_token: @token}.to_query }" %>
6 | 7 |<%= t("devise.mailer.invitation_instructions.ignore").html_safe %>
8 | -------------------------------------------------------------------------------- /db/migrate/20141127000723_add_team_id_to_apps.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddTeamIdToApps < ActiveRecord::Migration[4.2] 4 | def change 5 | add_reference :apps, :team, index: true, null: false, default: 1 6 | reversible do |dir| 7 | dir.up do 8 | change_column_default :apps, :team_id, nil 9 | end 10 | 11 | dir.down do 12 | change_column_default :apps, :team_id, 1 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/policies/registration_policy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class RegistrationPolicy < ApplicationPolicy 4 | def edit? 5 | !Rails.configuration.cuttlefish_read_only_mode 6 | end 7 | 8 | def update? 9 | edit? 10 | end 11 | 12 | def destroy? 13 | edit? 14 | end 15 | 16 | # Only allowed to register if you are the first admin 17 | def create? 18 | !Rails.configuration.cuttlefish_read_only_mode && Admin.first.nil? 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /db/migrate/20130414051352_add_postfix_queue_id_to_deliveries.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddPostfixQueueIdToDeliveries < ActiveRecord::Migration[4.2] 4 | def change 5 | add_column :deliveries, :postfix_queue_id, :string 6 | add_index :deliveries, :postfix_queue_id 7 | #Delivery.where(sent: true).joins(:email).update_all("deliveries.postfix_queue_id = emails.postfix_queue_id") 8 | remove_column :emails, :postfix_queue_id 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/graphql/mutations/remove_app.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutations 4 | class RemoveApp < Mutations::Base 5 | # TODO: Give descriptions for arguments and fields 6 | argument :id, ID, required: true 7 | 8 | field :errors, [Types::UserError], null: false 9 | 10 | def resolve(id:) 11 | AppServices::Destroy.call( 12 | id: id, current_admin: context[:current_admin] 13 | ) 14 | 15 | { errors: [] } 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/policies/deny_list_policy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class DenyListPolicy < ApplicationPolicy 4 | def destroy? 5 | app_ids = AppPolicy::Scope.new(user, App).resolve.pluck(:id) 6 | app_ids.include?(record.app_id) && !Rails.configuration.cuttlefish_read_only_mode 7 | end 8 | 9 | class Scope < Scope 10 | def resolve 11 | app_ids = AppPolicy::Scope.new(user, App).resolve.pluck(:id) 12 | scope.where(app_id: app_ids) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/graphql/types/count.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class Count < GraphQL::Schema::Object 5 | description "A bucket to hold the number of times a particular " \ 6 | "thing happens" 7 | 8 | field :count, Int, 9 | null: false, 10 | description: "The number of times the thing happens" 11 | field :name, String, 12 | null: false, 13 | description: "The name of the thing that happens" 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/migrate/20180816045458_add_legacy_dkim_selector_to_apps.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddLegacyDkimSelectorToApps < ActiveRecord::Migration[5.2] 4 | class App < ActiveRecord::Base 5 | end 6 | 7 | def change 8 | add_column :apps, :legacy_dkim_selector, :boolean, null: false, default: false 9 | reversible do |dir| 10 | dir.up do 11 | App.where(dkim_enabled: true).update_all(legacy_dkim_selector: true) 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/services/webhook_services/post_test_event_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | describe WebhookServices::PostTestEvent do 6 | it "sends a POST as json" do 7 | expect(RestClient).to receive(:post).with( 8 | "https://foo.com", 9 | { key: "abc123", test_event: {} }.to_json, 10 | { content_type: :json } 11 | ) 12 | described_class.call( 13 | url: "https://foo.com", 14 | key: "abc123" 15 | ) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: postgresql 3 | database: cuttlefish_development 4 | username: postgres 5 | password: cuttlefish 6 | host: db 7 | 8 | # Warning: The database defined as "test" will be erased and 9 | # re-generated from your development database when you run "rake". 10 | # Do not set this db to the same as development or production. 11 | test: 12 | adapter: postgresql 13 | database: cuttlefish_test 14 | username: postgres 15 | password: cuttlefish 16 | host: db 17 | -------------------------------------------------------------------------------- /db/migrate/20141128073822_add_team_id_to_black_lists.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddTeamIdToBlackLists < ActiveRecord::Migration[4.2] 4 | def change 5 | add_reference :black_lists, :team, index: true, null: false, default: 1 6 | reversible do |dir| 7 | dir.up do 8 | change_column_default :black_lists, :team_id, nil 9 | end 10 | 11 | dir.down do 12 | change_column_default :black_lists, :team_id, 1 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /db/migrate/20130514151224_get_rid_of_nil_app.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class GetRidOfNilApp < ActiveRecord::Migration[4.2] 4 | def change 5 | # This doesn't work anymore because App.default will fail at this 6 | # stage in the migrations. Could fix this properly but there is no 7 | # advantage to it. Instead just commenting out 8 | #Email.where(app_id: nil).update_all("app_id = #{App.default.id}") 9 | change_column :emails, :app_id, :integer, null: false 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/graphql/mutations/remove_admin.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutations 4 | class RemoveAdmin < Mutations::Base 5 | # TODO: Give descriptions for arguments and fields 6 | argument :id, ID, required: true 7 | 8 | field :admin, Types::Admin, null: true 9 | 10 | def resolve(id:) 11 | remove_admin = AdminServices::Destroy.call( 12 | id: id, current_admin: context[:current_admin] 13 | ) 14 | { admin: remove_admin.result } 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /db/migrate/20130418045343_add_counter_cache_to_deliveries.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddCounterCacheToDeliveries < ActiveRecord::Migration[4.2] 4 | def change 5 | add_column :deliveries, :open_events_count, :integer, null: false, default: 0 6 | # reset cached counts for deliveries with open_events only 7 | ids = Set.new 8 | OpenEvent.all.each {|e| ids << e.delivery_id} 9 | ids.each do |id| 10 | Delivery.reset_counters(id, :open_events) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/services/app_services/destroy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AppServices 4 | class Destroy < ApplicationService 5 | def initialize(current_admin:, id:) 6 | super() 7 | @current_admin = current_admin 8 | @id = id 9 | end 10 | 11 | def call 12 | app = App.find(id) 13 | Pundit.authorize(current_admin, app, :destroy?) 14 | app.destroy 15 | success! 16 | end 17 | 18 | private 19 | 20 | attr_reader :current_admin, :id 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/services/webhook_services/post_test_event.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module WebhookServices 4 | class PostTestEvent < ApplicationService 5 | def initialize(url:, key:) 6 | super() 7 | @url = url 8 | @key = key 9 | end 10 | 11 | def call 12 | RestClient.post( 13 | url, 14 | { key: key, test_event: {} }.to_json, 15 | { content_type: :json } 16 | ) 17 | end 18 | 19 | private 20 | 21 | attr_reader :url, :key 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/views/admins/passwords/new.html.haml: -------------------------------------------------------------------------------- 1 | = form_for(@admin, as: :admin, url: admin_password_path, html: { method: :post }) do |f| 2 | = render "devise/shared/error_messages", resource: @admin 3 | 4 | .control-group 5 | = f.label :email 6 | = f.email_field :email, class: "login-field", placeholder: "Enter your email" 7 | %span.login-field-icon.fui-man-16 8 | 9 | = f.submit "Send me reset password instructions", class: "btn btn-primary btn-large btn-block" 10 | 11 | = render partial: "devise/shared/links" 12 | -------------------------------------------------------------------------------- /app/graphql/types/user_error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class UserError < GraphQL::Schema::Object 5 | description "A user-readable error" 6 | 7 | field :message, String, 8 | null: false, description: "A description of the error" 9 | field :path, [String], 10 | null: true, description: "Which input value this error came from" 11 | # TODO: Make this an enum? 12 | field :type, String, 13 | null: false, description: "Type of error" 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /bin/rspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path("../spring", __FILE__) 4 | rescue LoadError 5 | end 6 | # 7 | # This file was generated by Bundler. 8 | # 9 | # The application 'rspec' is installed as part of a gem, and 10 | # this file is here to facilitate running it. 11 | # 12 | 13 | require 'pathname' 14 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile", 15 | Pathname.new(__FILE__).realpath) 16 | 17 | require 'rubygems' 18 | require 'bundler/setup' 19 | 20 | load Gem.bin_path('rspec-core', 'rspec') 21 | -------------------------------------------------------------------------------- /config/locales/en.bootstrap.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for English. Add more files in this directory for other locales. 2 | # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. 3 | 4 | en: 5 | helpers: 6 | actions: "Actions" 7 | links: 8 | back: "Back" 9 | cancel: "Cancel" 10 | confirm: "Are you sure?" 11 | destroy: "Delete" 12 | new: "New" 13 | titles: 14 | edit: "Edit" 15 | save: "Save" 16 | new: "New" 17 | delete: "Delete" 18 | -------------------------------------------------------------------------------- /spec/helpers/admins_helper_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | describe AppsHelper, type: :helper do 6 | describe ".email_with_name" do 7 | it do 8 | admin = Admin.new(email: "matthew@foo.com") 9 | expect(helper.email_with_name(admin)).to eq "matthew@foo.com" 10 | end 11 | 12 | it do 13 | admin = Admin.new(email: "matthew@foo.com", name: "Matthew") 14 | expect(helper.email_with_name(admin)).to eq "Matthewvašem
".encode(Encoding::ISO_8859_2) 12 | end 13 | end 14 | end 15 | 16 | it do 17 | mail2 = described_class.new(delivery: create(:delivery)).filter_mail(mail) 18 | expect(Nokogiri::HTML(mail2.html_part.decoded).at("p").inner_text).to eq( 19 | "vašem" 20 | ) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /db/migrate/20140711054108_add_counter_cache_to_delivery_links.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddCounterCacheToDeliveryLinks < ActiveRecord::Migration[4.2] 4 | def change 5 | add_column :delivery_links, :click_events_count, :integer, null: false, default: 0 6 | # reset cached counts for delivery_links with click_events only 7 | puts "Collecting ids..." 8 | ids = Set.new 9 | ClickEvent.pluck(:delivery_link_id).each {|id| ids << id} 10 | puts "Reseting counters..." 11 | ids.each do |id| 12 | DeliveryLink.reset_counters(id, :click_events) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/migrate/20200825041009_add_webhook_to_apps.rb: -------------------------------------------------------------------------------- 1 | class AddWebhookToApps < ActiveRecord::Migration[5.2] 2 | def change 3 | add_column :apps, :webhook_url, :string 4 | add_column :apps, :webhook_key, :string 5 | App.reset_column_information 6 | App.find_each do |app| 7 | app.set_webhook_key 8 | # If any of the DNS records have changed since the app was 9 | # initially setup the validation will fail. We don't want this 10 | # to happen. So, ignore validation. 11 | app.save!(validate: false) 12 | end 13 | change_column_null :apps, :webhook_key, false 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/api/addresses/from.graphql: -------------------------------------------------------------------------------- 1 | query ($from: String!, $limit: Int, $offset: Int) { 2 | emails(from: $from, limit: $limit, offset: $offset) { 3 | totalCount 4 | statistics { 5 | totalCount 6 | deliveredCount 7 | softBounceCount 8 | hardBounceCount 9 | notSentCount 10 | openRate 11 | clickRate 12 | } 13 | nodes { 14 | id 15 | to 16 | subject 17 | app { 18 | name 19 | } 20 | createdAt 21 | status 22 | opened 23 | clicked 24 | } 25 | } 26 | viewer { 27 | email 28 | siteAdmin 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/views/main/_reputation.html.haml: -------------------------------------------------------------------------------- 1 | - if listings.empty? 2 | %p This server is not listed on any known deny lists 3 | - else 4 | %p.text-error Unfortunately, this server is listed on some deny lists. That will likely severely impact your ability to send email. 5 | %table.table 6 | %thead 7 | %tr 8 | %th Deny list 9 | %th Result 10 | %tbody 11 | - listings.each do |listing| 12 | %tr 13 | %td= listing.dnsbl 14 | %td= listing.meaning 15 | %p 16 | Your server IP address 17 | %strong= ip 18 | %p 19 | (You will need this if you want to check things by hand) 20 | -------------------------------------------------------------------------------- /app/helpers/deliveries_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DeliveriesHelper 4 | def canonical_deliveries_path(app, status, search, key) 5 | if app 6 | app_deliveries_path(app, status: status, search: search, key: key) 7 | else 8 | deliveries_path(status: status, search: search, key: key) 9 | end 10 | end 11 | 12 | def clean_html_email_for_display(html) 13 | # Inline css so that email styling doesn't interfere with the cuttlefish ui 14 | # and only show the body of the html 15 | TransformHtml.new(html).inline_css_remove_style_blocks_and_replace_body_with_div 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/policies/admin_policy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AdminPolicy < ApplicationPolicy 4 | # This is currently unused 5 | def index? 6 | !user.nil? 7 | end 8 | 9 | def show? 10 | in_same_team? 11 | end 12 | 13 | def destroy? 14 | !Rails.configuration.cuttlefish_read_only_mode && in_same_team? 15 | end 16 | 17 | class Scope < Scope 18 | def resolve 19 | if user 20 | scope.where(team_id: user.team_id) 21 | else 22 | scope.none 23 | end 24 | end 25 | end 26 | 27 | private 28 | 29 | def in_same_team? 30 | user && user.team_id == record.team_id 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /app/graphql/types/team.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class Team < GraphQL::Schema::Object 5 | description "A team" 6 | 7 | field :admins, [Types::Admin], null: false do 8 | description "Admins that belong to this team, sorted " \ 9 | "alphabetically by name." 10 | end 11 | 12 | field :apps, [Types::App], null: true do 13 | description "Apps that belong to this team, " \ 14 | "sorted alphabetically by name." 15 | end 16 | 17 | def admins 18 | object.admins.order(:name) 19 | end 20 | 21 | def apps 22 | object.apps.order(:name) 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/views/domains/index.html.haml: -------------------------------------------------------------------------------- 1 | - content_for :title, "Problem Domains" 2 | 3 | .page-header 4 | %h1= yield :title 5 | %p 6 | Domains that have a high (or higher than usual) bounce rate might be blocking email. 7 | 8 | %p 9 | Here we show the domains that have hard bounced in the last 7 days 10 | 11 | %table.table 12 | %thead 13 | %tr 14 | %th Domain 15 | %th Number of hard bounces 16 | %th Number of deliveries 17 | %th Bounce rate 18 | %tbody 19 | - @domains.each do |d| 20 | %tr 21 | %td= d[:domain] 22 | %td= d[:hard_bounces] 23 | %td= d[:deliveries] 24 | %td= "%0.0f%%" % (100.0 * d[:bounce_rate]) -------------------------------------------------------------------------------- /db/migrate/20150112214714_drop_delayed_jobs_table.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class DropDelayedJobsTable < ActiveRecord::Migration[4.2] 4 | def up 5 | drop_table :delayed_jobs 6 | end 7 | 8 | def down 9 | create_table "delayed_jobs" do |t| 10 | t.integer "priority", default: 0 11 | t.integer "attempts", default: 0 12 | t.text "handler" 13 | t.text "last_error" 14 | t.datetime "run_at" 15 | t.datetime "locked_at" 16 | t.datetime "failed_at" 17 | t.string "locked_by" 18 | t.string "queue" 19 | t.datetime "created_at" 20 | t.datetime "updated_at" 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/assets/javascripts/apps.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | // TODO This is a bit long winded. Would be nice to do this more concisely 3 | if (!$("#app_click_tracking_enabled").is(":checked") && !$("#app_open_tracking_enabled").is(":checked")) { 4 | $("#app_custom_tracking_domain_input").css("display", "none"); 5 | } 6 | 7 | $("#app_click_tracking_enabled, #app_open_tracking_enabled").click(function() { 8 | if ($("#app_click_tracking_enabled").is(":checked") || $("#app_open_tracking_enabled").is(":checked")) { 9 | $("#app_custom_tracking_domain_input").show("fast"); 10 | } else { 11 | $("#app_custom_tracking_domain_input").hide("fast"); 12 | } 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /provisioning/roles/cuttlefish-app/templates/copy-cuttlefish-daemon-certs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | # Make sure the certificate and private key files are 6 | # never world readable, even just for an instant while 7 | # we're copying them 8 | umask 077 9 | 10 | cp "$RENEWED_LINEAGE/fullchain.pem" /srv/www/shared/ 11 | cp "$RENEWED_LINEAGE/privkey.pem" /srv/www/shared/ 12 | 13 | # Apply the proper file ownership and permissions for 14 | # the daemon to read its certificate and key. 15 | chown deploy /srv/www/shared/fullchain.pem /srv/www/shared/privkey.pem 16 | chmod 400 /srv/www/shared/fullchain.pem /srv/www/shared/privkey.pem 17 | 18 | systemctl restart cuttlefish-smtp.target > /dev/null 19 | -------------------------------------------------------------------------------- /provisioning/roles/cuttlefish-backup/.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: python 3 | python: "2.7" 4 | 5 | # Use the new container infrastructure 6 | sudo: false 7 | 8 | # Install ansible 9 | addons: 10 | apt: 11 | packages: 12 | - python-pip 13 | 14 | install: 15 | # Install ansible 16 | - pip install ansible 17 | 18 | # Check ansible version 19 | - ansible --version 20 | 21 | # Create ansible.cfg with correct roles_path 22 | - printf '[defaults]\nroles_path=../' >ansible.cfg 23 | 24 | script: 25 | # Basic role syntax check 26 | - ansible-playbook tests/test.yml -i tests/inventory --syntax-check 27 | 28 | notifications: 29 | webhooks: https://galaxy.ansible.com/api/v1/notifications/ -------------------------------------------------------------------------------- /app/controllers/teams_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class TeamsController < ApplicationController 4 | def index 5 | # TODO: Check for errors 6 | result = api_query 7 | @data = result.data 8 | @teams = @data.teams 9 | @cuttlefish_app = @data.cuttlefish_app 10 | @admin = OpenStruct.new(email: nil) 11 | end 12 | 13 | def invite 14 | result = api_query( 15 | email: params[:admin][:email], 16 | accept_url: accept_admin_invitation_url 17 | ) 18 | @data = result.data 19 | 20 | # TODO: Add some error checking 21 | flash[:notice] = "Invited #{params[:admin][:email]} to a new team" 22 | redirect_to teams_path 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | sudo: false 3 | 4 | services: 5 | - postgresql 6 | 7 | before_install: 8 | - gem install bundler:1.17.1 9 | 10 | before_script: 11 | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 12 | - chmod +x ./cc-test-reporter 13 | - ./cc-test-reporter before-build 14 | - cp config/database.travis.yml config/database.yml 15 | # We don't want to seed the database for tests 16 | - bundle exec rake db:create 17 | - bundle exec rake db:schema:load 18 | 19 | script: 20 | - bundle exec rake 21 | - bundle exec rubocop 22 | 23 | after_script: 24 | - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT 25 | -------------------------------------------------------------------------------- /app/views/admins/passwords/edit.html.erb: -------------------------------------------------------------------------------- 1 |Maybe you tried to change something you didn't have access to.
24 |If you are the application owner check the logs for more information.
25 | 26 | 27 | -------------------------------------------------------------------------------- /app/views/apps/index.html.haml: -------------------------------------------------------------------------------- 1 | .page-header 2 | %h1 Apps 3 | 4 | %p 5 | All emails are sent through apps, which represent the application that sends the email. 6 | Each App has its own settings and its own smtp authentication settings to contact the 7 | Cuttlefish SMTP server. 8 | 9 | %p 10 | = link_to "New App", new_app_path, class: "btn btn-primary" 11 | 12 | %table.table 13 | %thead 14 | %tr 15 | %th 16 | Name 17 | %tbody(data-provides="rowlink") 18 | - @apps.each do |app| 19 | - if app.dkim_enabled && app.dkim_dns_record.upgrade_required 20 | %tr.info 21 | %td 22 | %i.fa.fa-exclamation-circle 23 | = link_to app.name, app_path(app.id) 24 | - else 25 | %tr 26 | %td 27 | = link_to app.name, app_path(app.id) 28 | -------------------------------------------------------------------------------- /spec/models/delivery_link_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | describe DeliveryLink do 6 | describe "#add_click_event" do 7 | it "logs the link event with some info from the current request" do 8 | request = double( 9 | "Request", 10 | env: { "HTTP_USER_AGENT" => "some user agent info" }, 11 | referer: "http://foo.com", 12 | remote_ip: "1.2.3.4" 13 | ) 14 | delivery_link = create(:delivery_link) 15 | delivery_link.add_click_event(request) 16 | expect(delivery_link.click_events.count).to eq 1 17 | e = delivery_link.click_events.first 18 | expect(e.user_agent).to eq "some user agent info" 19 | expect(e.referer).to eq "http://foo.com" 20 | expect(e.ip).to eq "1.2.3.4" 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/graphql/mutations/invite_team.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutations 4 | class InviteTeam < Mutations::Base 5 | argument :accept_url, String, required: true 6 | argument :email, String, required: true 7 | 8 | field :admin, Types::Admin, null: true 9 | field :errors, [Types::UserError], null: false 10 | 11 | def resolve(email:, accept_url:) 12 | Pundit.authorize(context[:current_admin], :team, :invite?) 13 | 14 | # TODO: Put these in a transaction 15 | team = Team.create! 16 | admin = Admin.invite!( 17 | { email: email, team_id: team.id }, 18 | context[:current_admin], 19 | accept_url: accept_url 20 | ) 21 | 22 | { admin: admin, errors: user_errors_from_form_errors(admin.errors, ["attributes"]) } 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/lib/dkim_dns_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | describe DkimDns do 6 | let(:dkim_dns) do 7 | described_class.new( 8 | private_key: OpenSSL::PKey::RSA.new(2048), 9 | domain: "foo.com", 10 | selector: "cuttlefish" 11 | ) 12 | end 13 | 14 | describe "#dkim_dns_value" do 15 | it "gives me the dns record value" do 16 | # Test certain invariants 17 | expect(dkim_dns.dkim_dns_value[0..8]).to eq "k=rsa; p=" 18 | expect(dkim_dns.dkim_dns_value.count('"')).to eq 0 19 | expect(dkim_dns.dkim_dns_value.length).to eq 401 20 | end 21 | end 22 | 23 | describe "#dkim_domain" do 24 | it "returns the fully qualified domain name" do 25 | expect(dkim_dns.dkim_domain).to eq "cuttlefish._domainkey.foo.com" 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /app/assets/stylesheets/emails.css.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the Emails controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | 5 | table td { 6 | vertical-align: top; 7 | ul { 8 | list-style-type: none; 9 | margin-left: 0; 10 | } 11 | } 12 | 13 | .pagination, .pager { 14 | display: inline-block; 15 | float: right; 16 | margin-top: 0; 17 | } 18 | 19 | p.count { 20 | margin-top: 20px; 21 | } 22 | 23 | span.label a { 24 | text-decoration: none; 25 | color: white; 26 | } 27 | 28 | ul.nav.nav-pills a { 29 | text-decoration: none; 30 | } 31 | 32 | table#email-headers .key { 33 | width: 85px; 34 | text-align: right; 35 | padding-right: 15px; 36 | } 37 | 38 | iframe { 39 | border: 0; 40 | width: 100%; 41 | height: 700px; 42 | } -------------------------------------------------------------------------------- /app/controllers/domains_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class DomainsController < ApplicationController 4 | def index 5 | result = api_query since: 1.week.ago.utc.iso8601 6 | @data = result.data 7 | # Going to do a little rejigging of the data 8 | hard_bounce_counts = @data.emails.statistics.hard_bounce_count_by_to_domain.map{|c| [c.name, c.count]}.to_h 9 | delivered_counts = @data.emails.statistics.delivered_count_by_to_domain.map{|c| [c.name, c.count]}.to_h 10 | @domains = hard_bounce_counts.map do |domain, hard_bounces| 11 | deliveries = (delivered_counts[domain] || 0) 12 | { 13 | domain: domain, 14 | hard_bounces: hard_bounces, 15 | deliveries: deliveries, 16 | bounce_rate: hard_bounces.to_f / (hard_bounces + deliveries) 17 | } 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /db/migrate/20141126054742_add_team_id_to_admins.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddTeamIdToAdmins < ActiveRecord::Migration[4.2] 4 | def change 5 | reversible do |dir| 6 | dir.up do 7 | # Team that we add pre-existing admins and apps to 8 | Team.create!(id: 1) 9 | end 10 | 11 | dir.down do 12 | Team.delete(1) 13 | end 14 | end 15 | 16 | add_reference :admins, :team, index: true, null: false, default: 1 17 | #change_column :admins, :team_id, :integer, null: false 18 | reversible do |dir| 19 | dir.up do 20 | change_column_default :admins, :team_id, nil 21 | end 22 | 23 | dir.down do 24 | change_column_default :admins, :team_id, 1 25 | end 26 | end 27 | #t.integer "team_id", default: 1, null: false 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/policies/test_email_policy_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | describe TestEmailPolicy do 6 | subject { described_class.new(user, nil) } 7 | 8 | context "when normal user" do 9 | let(:user) { create(:admin) } 10 | 11 | it { is_expected.to permit(:new) } 12 | it { is_expected.to permit(:create) } 13 | 14 | context "when in read only mode" do 15 | before do 16 | allow(Rails.configuration).to receive(:cuttlefish_read_only_mode).and_return(true) 17 | end 18 | 19 | it { is_expected.not_to permit(:new) } 20 | it { is_expected.not_to permit(:create) } 21 | end 22 | end 23 | 24 | context "when unauthenticated user" do 25 | let(:user) { nil } 26 | 27 | it { is_expected.not_to permit(:new) } 28 | it { is_expected.not_to permit(:create) } 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /app/views/documentation/_general.html.haml: -------------------------------------------------------------------------------- 1 | %h3 General 2 | 3 | %p 4 | These are the details that you need to be able to send email via Cuttlefish for the #{app.name} App. 5 | Specific instructions for sending mail from different frameworks and languages is further down the page. 6 | 7 | %table.table.table-condensed 8 | %tr 9 | %td Protocol 10 | %td 11 | %strong= link_to "SMTP", "http://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol" 12 | %tr 13 | %td Host 14 | %td 15 | %strong= app.smtp_server.hostname 16 | %tr 17 | %td Port 18 | %td 19 | %strong= app.smtp_server.port 20 | %tr 21 | %td Username 22 | %td 23 | %strong= app.smtp_server.username 24 | %tr 25 | %td Password 26 | %td 27 | %strong= app.smtp_server.password 28 | %tr 29 | %td Authentication 30 | %td 31 | %strong plain 32 | -------------------------------------------------------------------------------- /app/graphql/mutations/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutations 4 | class Base < GraphQL::Schema::Mutation 5 | def user_errors_from_form_errors(errors, root_path) 6 | user_errors = [] 7 | # Convert Rails model errors into GraphQL-ready error hashes 8 | errors.attribute_names.each do |attribute| 9 | m = errors.messages[attribute] 10 | d = errors.details[attribute] 11 | m.zip(d).each do |message, detail| 12 | # This is the GraphQL argument which corresponds to the 13 | # validation error: 14 | path = root_path + [attribute.to_s.camelize(:lower)] 15 | user_errors << { 16 | path: path, 17 | message: message, 18 | type: detail[:error].to_s.upcase 19 | } 20 | end 21 | end 22 | user_errors 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/graphql/types/smtp_server.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class SmtpServer < GraphQL::Schema::Object 5 | description "Details needed to send email to the Cuttlefish SMTP server" 6 | field :hostname, String, 7 | null: false, 8 | description: "The hostname" 9 | field :password, String, 10 | null: false, 11 | description: "The password to authenticate", method: :smtp_password 12 | field :port, Int, 13 | null: false, 14 | description: "The port" 15 | field :username, String, 16 | null: false, 17 | description: "The username to authenticate", method: :smtp_username 18 | 19 | def hostname 20 | Rails.configuration.cuttlefish_smtp_host 21 | end 22 | 23 | def port 24 | Rails.configuration.cuttlefish_smtp_port 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path. 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | # Add Yarn node_modules folder to the asset load path. 9 | Rails.application.config.assets.paths << Rails.root.join('node_modules') 10 | 11 | # Precompile additional assets. 12 | # application.js, application.css, and all non-JS/CSS in the app/assets 13 | # folder are already added. 14 | # Rails.application.config.assets.precompile += %w( admin.js admin.css ) 15 | 16 | # For bootstrap-sass gem 17 | Rails.application.config.assets.precompile += %w[*.png *.jpg *.jpeg *.gif] 18 | Rails.application.config.assets.precompile += %w[*.woff *.svg *.eot *.ttf] 19 | -------------------------------------------------------------------------------- /app/graphql/mutations/reset_password_by_token.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutations 4 | class ResetPasswordByToken < Mutations::Base 5 | argument :password, String, required: true 6 | argument :token, String, required: true 7 | 8 | # Returns a JSON web token so the user has the option to automatically 9 | # log in after a password reset 10 | field :errors, [Types::UserError], null: false 11 | field :token, String, null: false 12 | 13 | def resolve(password:, token:) 14 | admin = Admin.reset_password_by_token( 15 | reset_password_token: token, 16 | password: password 17 | ) 18 | token = JWT.encode({ admin_id: admin.id, exp: Time.now.to_i + 3600 }, ENV.fetch("JWT_SECRET", nil), "HS512") 19 | 20 | { token: token, errors: user_errors_from_form_errors(admin.errors, ["attributes"]) } 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /provisioning/roles/cuttlefish-app/templates/env: -------------------------------------------------------------------------------- 1 | HOME=/home/deploy 2 | GOOGLE_ANALYTICS_CODE={{ google_analytics_code }} 3 | SECRET_KEY_BASE={{ secret_key_base }} 4 | DEVISE_SECRET_KEY={{ devise_secret_key }} 5 | NEW_RELIC_LICENSE_KEY={{ new_relic_license_key }} 6 | NEW_RELIC_APP_NAME={{ new_relic_app_name }} 7 | HONEYBADGER_API_KEY={{ honeybadger_api_key }} 8 | CUTTLEFISH_HASH_SALT="{{ cuttlefish_hash_salt }}" 9 | POSTFIX_LOG_PATH=/var/log/mail/mail.log 10 | CUTTLEFISH_SMTP_HOST=cuttlefish.oaf.org.au 11 | CUTTLEFISH_DOMAIN=cuttlefish.oaf.org.au 12 | S3_BUCKET="{{ s3_bucket }}" 13 | AWS_ACCESS_KEY_ID="{{ aws_access_key_id }}" 14 | AWS_SECRET_ACCESS_KEY="{{ aws_secret_access_key }}" 15 | CUTTLEFISH_DOMAIN_CERT_CHAIN_FILE=/srv/www/shared/fullchain.pem 16 | CUTTLEFISH_DOMAIN_PRIVATE_KEY_FILE=/srv/www/shared/privkey.pem 17 | NEW_RELIC_AGENT_ENABLED={{ new_relic_agent_enabled }} 18 | JWT_SECRET={{ jwt_secret }} 19 | -------------------------------------------------------------------------------- /lib/api/addresses/to.graphql: -------------------------------------------------------------------------------- 1 | query ($to: String!, $limit: Int, $offset: Int) { 2 | emails(to: $to, limit: $limit, offset: $offset) { 3 | totalCount 4 | statistics { 5 | totalCount 6 | deliveredCount 7 | softBounceCount 8 | hardBounceCount 9 | notSentCount 10 | openRate 11 | clickRate 12 | } 13 | nodes { 14 | id 15 | from 16 | subject 17 | app { 18 | name 19 | } 20 | createdAt 21 | status 22 | opened 23 | clicked 24 | } 25 | } 26 | blockedAddresses(address: $to) { 27 | nodes { 28 | id 29 | becauseOfDeliveryEvent { 30 | time 31 | extendedStatus 32 | email { 33 | id 34 | } 35 | } 36 | permissions { 37 | destroy 38 | } 39 | } 40 | } 41 | viewer { 42 | email 43 | siteAdmin 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /spec/lib/filters/mailer_header_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | describe Filters::MailerHeader do 6 | let(:mail) do 7 | Mail.new do 8 | text_part do 9 | body "An email with some text and headers" 10 | end 11 | end 12 | end 13 | let(:filter) { described_class.new(version: APP_VERSION) } 14 | 15 | describe "#data" do 16 | context "when version 1.2 of the app" do 17 | before { filter.version = "1.2" } 18 | 19 | it "adds an X-Mailer header" do 20 | expect(filter.filter_mail(mail).header["X-Mailer"].to_s).to eq( 21 | "Cuttlefish 1.2" 22 | ) 23 | end 24 | 25 | it "does not alter anything else" do 26 | expect(filter.filter_mail(mail).text_part.decoded).to eq( 27 | "An email with some text and headers" 28 | ) 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /config/initializers/formtastic.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Formtastic::Helpers::FormHelper.builder = FormtasticBootstrap::FormBuilder 4 | 5 | # Monkey patching this old version of formtastic so that it works with ruby 3.0 6 | # We're stuck on this old version because the bootstrap-formtastic gem that still works 7 | # with bootstrap 2 doesn't work with later formtastic. Ugh... 8 | # I guess this is what you get when you're stuck on old versions of things. 9 | module Formtastic 10 | module I18n 11 | class << self 12 | def translate(*args) 13 | key = args.shift.to_sym 14 | options = args.extract_options! 15 | options.reverse_merge!(:default => DEFAULT_VALUES[key]) 16 | options[:scope] = [DEFAULT_SCOPE, options[:scope]].flatten.compact 17 | ::I18n.translate(key, *args, **options) 18 | end 19 | alias :t :translate 20 | end 21 | end 22 | end -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |You may have mistyped the address or the page may have moved.
24 |If you are the application owner check the logs for more information.
26 | 27 | 28 | -------------------------------------------------------------------------------- /app/policies/application_policy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationPolicy 4 | attr_reader :user, :record 5 | 6 | def initialize(user, record) 7 | @user = user 8 | @record = record 9 | end 10 | 11 | def index? 12 | false 13 | end 14 | 15 | def show? 16 | scope.exists?(id: record.id) 17 | end 18 | 19 | def create? 20 | false 21 | end 22 | 23 | def new? 24 | create? 25 | end 26 | 27 | def update? 28 | false 29 | end 30 | 31 | def edit? 32 | update? 33 | end 34 | 35 | def destroy? 36 | false 37 | end 38 | 39 | def scope 40 | Pundit.policy_scope!(user, record.class) 41 | end 42 | 43 | class Scope 44 | attr_reader :user, :scope 45 | 46 | def initialize(user, scope) 47 | @user = user 48 | @scope = scope 49 | end 50 | 51 | def resolve 52 | scope 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /app/controllers/main_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class MainController < ApplicationController 4 | def index 5 | result = api_query 6 | @data = result.data 7 | end 8 | 9 | def status_counts 10 | result = api_query since1: 1.day.ago.utc.iso8601, 11 | since2: 1.week.ago.utc.iso8601 12 | @stats_today = result.data.emails1.statistics 13 | @stats_this_week = result.data.emails2.statistics 14 | 15 | render partial: "status_counts", locals: { loading: false } 16 | end 17 | 18 | def reputation 19 | if request.xhr? 20 | result = api_query :partial, {} 21 | @data = result.data 22 | 23 | render partial: "reputation", locals: { 24 | listings: @data.dnsbl, 25 | ip: @data.configuration.ip_address 26 | } 27 | else 28 | result = api_query 29 | @data = result.data 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/pager_renderer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class PagerRenderer < WillPaginate::ActionView::LinkRenderer 4 | def to_html 5 | tag(:ul, previous_page + next_page, class: "pager") 6 | end 7 | 8 | def previous_page 9 | num = @collection.current_page > 1 && (@collection.current_page - 1) 10 | previous_or_next_page(num, @options[:previous_label], "previous") 11 | end 12 | 13 | def next_page 14 | num = @collection.current_page < total_pages && (@collection.current_page + 1) 15 | previous_or_next_page(num, @options[:next_label], "next") 16 | end 17 | 18 | def previous_or_next_page(page, text, classname) 19 | text += tag(:span, @options[:text]).html_safe if classname == "previous" 20 | if page 21 | tag(:li, link(text, page), class: classname) 22 | else 23 | tag(:li, tag(:a, text, href: "#"), class: "#{classname} disabled") 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/bootstrap-extended/bootstrap-rowlink.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Bootstrap.js by @mdo and @fat, extended by @ArnoldDaniels. 3 | * plugins: bootstrap-rowlink.js 4 | * Copyright 2012 Twitter, Inc. 5 | * http://www.apache.org/licenses/LICENSE-2.0.txt 6 | */ 7 | !function(e){var t=function(t,n){n=e.extend({},e.fn.rowlink.defaults,n);var r=t.nodeName.toLowerCase()=="tr"?e(t):e(t).find("tr:has(td)");r.each(function(){var t=e(this).find(n.target).first();if(!t.length)return;var r=t.attr("href");e(this).find("td").not(".nolink").click(function(){window.location=r}),e(this).addClass("rowlink"),t.replaceWith(t.html())})};e.fn.rowlink=function(n){return this.each(function(){var r=e(this),i=r.data("rowlink");i||r.data("rowlink",i=new t(this,n))})},e.fn.rowlink.defaults={target:"a"},e.fn.rowlink.Constructor=t,e(function(){e('[data-provide="rowlink"],[data-provides="rowlink"]').each(function(){e(this).rowlink(e(this).data())})})}(window.jQuery) -------------------------------------------------------------------------------- /vendor/assets/stylesheets/bootstrap-extended/bootstrap-rowlink.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v2.3.1-j6 3 | * 4 | * Copyright 2012 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world by @mdo and @fat, extended by @ArnoldDaniels. 9 | */ 10 | .clearfix{*zoom:1;}.clearfix:before,.clearfix:after{display:table;content:"";line-height:0;} 11 | .clearfix:after{clear:both;} 12 | .hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0;} 13 | .input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;} 14 | tr.rowlink td{cursor:pointer;}tr.rowlink td.nolink{cursor:auto;} 15 | .table tbody tr.rowlink:hover td{background-color:#cfcfcf;} 16 | a.rowlink{color:inherit;font:inherit;text-decoration:inherit;} 17 | -------------------------------------------------------------------------------- /app/views/documentation/_rails.html.haml: -------------------------------------------------------------------------------- 1 | %h3 Using Rails 2 | 3 | %p 4 | = link_to "Ruby on Rails", "http://rubyonrails.org" 5 | is an open-source web framework that's optimized for programmer 6 | happiness and sustainable productivity. 7 | %p 8 | Depending on whether you want to send emails via Cuttlefish in your development or production environment either editconfig/environments/development.rb or config/environments/production.rb and add the following:
9 |
10 | :coderay
11 | #!ruby
12 |
13 | # Send #{app.name} mails to Cuttlefish (see http://cuttlefish.io)
14 | config.action_mailer.delivery_method = :smtp
15 | config.action_mailer.smtp_settings = {
16 | :address => '#{app.smtp_server.hostname}',
17 | :port => #{app.smtp_server.port},
18 | :user_name => '#{app.smtp_server.username}',
19 | :password => '#{app.smtp_server.password}',
20 | :authentication => :plain
21 | }
22 |
--------------------------------------------------------------------------------
/app/views/apps/new.html.haml:
--------------------------------------------------------------------------------
1 | .page-header
2 | %h1 New App
3 |
4 | = semantic_form_for @app, as: :app, url: apps_path, html: {class: "form-horizontal"} do |f|
5 | = f.semantic_errors
6 | = f.inputs "Basic" do
7 | = f.input :name, placeholder: "Angelfish", input_html: {class: "span6"}
8 | = f.inputs "Tracking" do
9 | = f.input :click_tracking_enabled, as: :boolean, hint: "Rewrite all the links in html emails so that you can find out when users click them"
10 | = f.input :open_tracking_enabled, as: :boolean, hint: "Adds a small transparent image to the end of html emails so that you can find out when an email is opened"
11 | = f.input :custom_tracking_domain, placeholder: "email.angelfishsocute.com", hint: "Leave blank if you want to use default domain (#{Rails.configuration.cuttlefish_domain}) for open and link tracking. You can set this later too."
12 | = f.actions do
13 | = f.action :submit, label: "Create App"
14 |
--------------------------------------------------------------------------------
/lib/bootstrap_link_renderer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Adapted from https://github.com/yrgoldteeth/bootstrap-will_paginate/blob/master/config/initializers/will_paginate.rb
4 |
5 | class BootstrapLinkRenderer < WillPaginate::ActionView::LinkRenderer
6 | def html_container(html)
7 | tag :div, tag(:ul, html), container_attributes
8 | end
9 |
10 | def page_number(page)
11 | tag :li,
12 | link(page, page, rel: rel_value(page)),
13 | class: ("active" if page == current_page)
14 | end
15 |
16 | def gap
17 | tag :li,
18 | link("…".html_safe, "#"),
19 | class: "disabled"
20 | end
21 |
22 | def previous_or_next_page(page, text, classname)
23 | tag :li,
24 | link(text, page || "#"),
25 | class: [
26 | (classname.split("_").first if @options[:page_links]),
27 | ("disabled" unless page)
28 | ].join(" ")
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/spec/controllers/admins/sessions_controller_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | describe Admins::SessionsController, type: :controller do
6 | before do
7 | request.env["devise.mapping"] = Devise.mappings[:admin]
8 | end
9 |
10 | context "when request is over https" do
11 | context "when this is a fresh install and there are no admins registered" do
12 | it "redirects to the registration page" do
13 | get :new
14 | expect(response).to redirect_to new_admin_registration_url
15 | end
16 | end
17 |
18 | context "when there is one admin already registered" do
19 | before do
20 | team = Team.create!
21 | team.admins.create!(email: "foo@bar.com", password: "guess this")
22 | end
23 |
24 | it "does not redirect" do
25 | get :new
26 | expect(response).to be_successful
27 | end
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/spec/policies/invitation_policy_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | describe InvitationPolicy do
6 | subject { described_class.new(user, nil) }
7 |
8 | context "when normal user" do
9 | let(:user) { create(:admin) }
10 |
11 | it { is_expected.to permit(:create) }
12 | it { is_expected.to permit(:update) }
13 |
14 | context "when in read only mode" do
15 | before do
16 | allow(Rails.configuration).to receive(:cuttlefish_read_only_mode).and_return(true)
17 | end
18 |
19 | it { is_expected.not_to permit(:create) }
20 | it { is_expected.not_to permit(:update) }
21 | end
22 | end
23 |
24 | context "when unauthenticated user" do
25 | let(:user) { nil }
26 |
27 | it { is_expected.not_to permit(:create) }
28 | # Because this is for accepting an invitation which is unauthenticated
29 | it { is_expected.to permit(:update) }
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/bin/update:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require 'fileutils'
3 | include FileUtils
4 |
5 | # path to your application root.
6 | APP_ROOT = File.expand_path('..', __dir__)
7 |
8 | def system!(*args)
9 | system(*args) || abort("\n== Command #{args} failed ==")
10 | end
11 |
12 | chdir APP_ROOT do
13 | # This script is a way to update your development environment automatically.
14 | # Add necessary update steps to this file.
15 |
16 | puts '== Installing dependencies =='
17 | system! 'gem install bundler --conservative'
18 | system('bundle check') || system!('bundle install')
19 |
20 | # Install JavaScript dependencies if using Yarn
21 | # system('bin/yarn')
22 |
23 | puts "\n== Updating database =="
24 | system! 'bin/rails db:migrate'
25 |
26 | puts "\n== Removing old logs and tempfiles =="
27 | system! 'bin/rails log:clear tmp:clear'
28 |
29 | puts "\n== Restarting application server =="
30 | system! 'bin/rails restart'
31 | end
32 |
--------------------------------------------------------------------------------
/db/migrate/20130419081443_devise_invitable_add_to_admins.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class DeviseInvitableAddToAdmins < ActiveRecord::Migration[4.2]
4 | def up
5 | change_table :admins do |t|
6 | t.string :invitation_token, limit: 60
7 | t.datetime :invitation_sent_at
8 | t.datetime :invitation_accepted_at
9 | t.integer :invitation_limit
10 | t.references :invited_by, polymorphic: true
11 | t.index :invitation_token, unique: true # for invitable
12 | t.index :invited_by_id
13 | end
14 |
15 | # And allow null encrypted_password and password_salt:
16 | change_column_null :admins, :encrypted_password, true
17 | end
18 |
19 | def down
20 | change_table :admins do |t|
21 | t.remove_references :invited_by, polymorphic: true
22 | t.remove :invitation_limit, :invitation_sent_at, :invitation_accepted_at, :invitation_token
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format. Inflections
4 | # are locale specific, and you may define rules for as many different
5 | # locales as you wish. All of these examples are active by default:
6 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
7 | # inflect.plural /^(ox)$/i, '\1en'
8 | # inflect.singular /^(ox)en/i, '\1'
9 | # inflect.irregular 'person', 'people'
10 | # inflect.uncountable %w( fish sheep )
11 | # end
12 |
13 | # These inflection rules are supported but not enabled by default:
14 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
15 | # inflect.acronym 'RESTful'
16 | # end
17 |
18 | ActiveSupport::Inflector.inflections(:en) do |inflect|
19 | # Doing these for the benefit of zeitwerk
20 | inflect.acronym "IP"
21 | inflect.acronym "DNSBL"
22 | inflect.acronym "SSL"
23 | inflect.acronym "DSN"
24 | end
25 |
--------------------------------------------------------------------------------
/app/views/deliveries/_open_events.html.haml:
--------------------------------------------------------------------------------
1 | %table.table.table-condensed
2 | %thead
3 | %tr
4 | %th Time
5 | %th Client
6 | %th OS
7 | %th IP
8 | %th Location
9 | %th
10 | %abbr{title: "Internet Service Provider"} ISP
11 | %th Org
12 | %tbody
13 | - delivery.open_events.each do |event|
14 | %tr
15 | %td
16 | = time_ago_in_words(event.created_at)
17 | ago
18 | %td
19 | = event.user_agent.family
20 | - if event.user_agent.version
21 | %span.muted (#{event.user_agent.version})
22 | %td
23 | = event.os.family
24 | - if event.os.version
25 | %span.muted (#{event.os.version})
26 | %td= event.ip.address
27 | - if event.ip.info
28 | %td= "#{event.ip.info.city}, #{event.ip.info.region_name}, #{event.ip.info.country}"
29 | %td= event.ip.info.isp
30 | %td= event.ip.info.org
31 |
--------------------------------------------------------------------------------
/spec/controllers/invitations_controller_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | describe InvitationsController, type: :controller do
6 | before do
7 | request.env["HTTPS"] = "on"
8 | end
9 |
10 | let(:team) { Team.create! }
11 | let(:admin) do
12 | team.admins.create!(email: "foo@bar.com", password: "guess this")
13 | end
14 |
15 | describe "#create" do
16 | context "when signed in" do
17 | before { sign_in admin }
18 |
19 | it "invites a user by their email and make them part of the team" do
20 | expect(Admin).to receive(:invite!).with(
21 | { email: "matthew@foo.bar", team_id: team.id },
22 | admin,
23 | accept_url: "https://test.host/admins/invitation/accept"
24 | ).and_call_original
25 | post :create, params: { admin: { email: "matthew@foo.bar" } }
26 | expect(response).to redirect_to admins_url
27 | end
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/app/graphql/cuttlefish_schema.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class CuttlefishSchema < GraphQL::Schema
4 | mutation(Types::MutationType)
5 | query(Types::QueryType)
6 |
7 | use GraphQL::Guard.new(
8 | not_authorized: lambda do |type, field|
9 | GraphQL::ExecutionError.new(
10 | "Not authorized to access #{type}.#{field}",
11 | extensions: { "type" => "NOT_AUTHORIZED" }
12 | )
13 | end
14 | )
15 | use BatchLoader::GraphQL
16 |
17 | rescue_from Pundit::NotAuthorizedError do |_e, _object, _arguments, _context, field|
18 | GraphQL::ExecutionError.new(
19 | "Not authorized to access #{field.owner.graphql_name}.#{field.name}",
20 | extensions: { "type" => "NOT_AUTHORIZED" }
21 | )
22 | end
23 |
24 | rescue_from ActiveRecord::RecordNotFound do |_exception|
25 | GraphQL::ExecutionError.new(
26 | "We couldn't find what you were looking for",
27 | extensions: { "type" => "NOT_FOUND" }
28 | )
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/app/views/admins/registrations/new.html.haml:
--------------------------------------------------------------------------------
1 | = form_for(@admin, as: :admin, url: admin_registration_path) do |f|
2 | = render "devise/shared/error_messages", resource: @admin
3 |
4 | %h2 You're special!
5 | %p
6 | As the first admin of Cuttlefish you get to create your own account. After this,
7 | only admins can invite new admins.
8 |
9 | .control-group
10 | = f.label :name
11 | .controls
12 | = f.text_field :name, class: "login-field", placeholder: "Enter your name"
13 | %span.login-field-icon.fui-heart-16
14 |
15 | .control-group
16 | = f.label :email
17 | = f.email_field :email, class: "login-field", placeholder: "Enter your email"
18 | %span.login-field-icon.fui-man-16
19 |
20 | .control-group
21 | = f.label :password
22 | = f.password_field :password, class: "login-field", placeholder: "Password"
23 | %span.login-field-icon.fui-lock-16
24 |
25 | = f.submit "Sign up", class: "btn btn-primary btn-large btn-block"
26 |
--------------------------------------------------------------------------------
/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Files in the config/locales directory are used for internationalization
2 | # and are automatically loaded by Rails. If you want to use locales other
3 | # than English, add the necessary files in this directory.
4 | #
5 | # To use the locales, use `I18n.t`:
6 | #
7 | # I18n.t 'hello'
8 | #
9 | # In views, this is aliased to just `t`:
10 | #
11 | # <%= t('hello') %>
12 | #
13 | # To use a different locale, set it with `I18n.locale`:
14 | #
15 | # I18n.locale = :es
16 | #
17 | # This would use the information in config/locales/es.yml.
18 | #
19 | # The following keys must be escaped otherwise they will not be retrieved by
20 | # the default I18n backend:
21 | #
22 | # true, false, on, off, yes, no
23 | #
24 | # Instead, surround them with single quotes.
25 | #
26 | # en:
27 | # 'true': 'foo'
28 | #
29 | # To learn more, please read the Rails Internationalization guide
30 | # available at https://guides.rubyonrails.org/i18n.html.
31 |
32 | en:
33 | hello: "Hello world"
34 |
--------------------------------------------------------------------------------
/.devcontainer/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.8'
2 | services:
3 | # Update this to the name of the service you want to work with in your docker-compose.yml file
4 | web:
5 | # Uncomment if you want to override the service's Dockerfile to one in the .devcontainer
6 | # folder. Note that the path of the Dockerfile and context is relative to the *primary*
7 | # docker-compose.yml file (the first in the devcontainer.json "dockerComposeFile"
8 | # array). The sample below assumes your primary file is in the root of your project.
9 | #
10 | # build:
11 | # context: .
12 | # dockerfile: .devcontainer/Dockerfile
13 |
14 | volumes:
15 | # Update this to wherever you want VS Code to mount the folder of your project
16 | - ../..:/workspaces:cached
17 |
18 | # Uncomment the next four lines if you will use a ptrace-based debugger like C++, Go, and Rust.
19 | # cap_add:
20 | # - SYS_PTRACE
21 | # security_opt:
22 | # - seccomp:unconfined
23 |
24 |
--------------------------------------------------------------------------------
/app/views/clients/index.html.haml:
--------------------------------------------------------------------------------
1 | - content_for :title, "Email Clients"
2 |
3 | .page-header
4 | %h1= yield :title
5 | %p
6 | Email clients that people are using, figured out from the user agent of open events.
7 | It doesn't detect web mail clients other than Gmail.
8 |
9 | %p
10 | Note that if people have image loading disabled or are using a text-only
11 | email clients they are not included in these results.
12 |
13 | .btn-group
14 | %button.btn.dropdown-toggle(data-toggle="dropdown")
15 | - if @app.nil?
16 | All Apps
17 | - else
18 | = @app.name
19 | %span.caret
20 | %ul.dropdown-menu
21 | %li= link_to "All Apps", clients_path
22 | - @apps.each do |app|
23 | %li= link_to app.name, app_clients_path(app.id)
24 |
25 | %table.table
26 | %thead
27 | %tr
28 | %th Client
29 | %th Count
30 | %tbody
31 | - @client_counts.each do |client_count|
32 | %tr
33 | %td= client_count.name
34 | %td= client_count.count
35 |
--------------------------------------------------------------------------------
/app/models/address.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Address < ApplicationRecord
4 | has_many :emails_sent, class_name: "Email",
5 | foreign_key: "from_address_id",
6 | inverse_of: :from_address,
7 | dependent: :restrict_with_exception
8 | has_many :deliveries, dependent: :restrict_with_exception
9 | has_many :postfix_log_lines, through: :deliveries
10 | has_many :emails_received, through: :deliveries, source: :email
11 |
12 | extend FriendlyId
13 | friendly_id :text
14 |
15 | # Extract just the domain part of the address
16 | def domain
17 | text.split("@")[1]
18 | end
19 |
20 | def emails
21 | Email.joins(:from_address, :to_addresses)
22 | .where("addresses.id = ? OR deliveries.address_id = ?", id, id)
23 | end
24 |
25 | def status
26 | most_recent_log_line = postfix_log_lines.order("time DESC").first
27 | most_recent_log_line ? most_recent_log_line.status : "sent"
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/app/graphql/mutations/accept_admin_invitation.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Mutations
4 | class AcceptAdminInvitation < Mutations::Base
5 | argument :name, String, required: false
6 | argument :password, String, required: true
7 | argument :token, String, required: true
8 |
9 | field :errors, [Types::UserError], null: false
10 | field :token, String, null: true
11 |
12 | def resolve(name:, password:, token:)
13 | Pundit.authorize(context[:current_admin], :invitation, :update?)
14 | admin = Admin.accept_invitation!(
15 | invitation_token: token,
16 | name: name,
17 | password: password
18 | )
19 | if admin.errors.empty?
20 | token = JWT.encode({ admin_id: admin.id, exp: Time.now.to_i + 3600 }, ENV.fetch("JWT_SECRET", nil), "HS512")
21 | { token: token, errors: [] }
22 | else
23 | { token: nil, errors: user_errors_from_form_errors(admin.errors, ["attributes"]) }
24 | end
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/app/views/teams/index.html.haml:
--------------------------------------------------------------------------------
1 | .page-header
2 | %h1 Teams
3 | %p
4 | Teams are self-contained groups of administrators who all have access to the same set of apps
5 | but can't see apps or emails belonging to other teams.
6 | A team most likely corresponds to an organisation.
7 |
8 |
9 | %ul
10 | - @teams.each do |team|
11 | %li
12 | = pluralize(team.admins.size, "administrator")
13 | (#{team.admins.map{|a| mail_to h(a.email), h(a.display_name)}.to_sentence.html_safe})
14 | %ul
15 | - team.apps.each do |app|
16 | %li= link_to app.name, app_path(app.id)
17 | %li
18 | Cuttlefish team
19 | %ul
20 | %li= link_to @cuttlefish_app.name, app_path(@cuttlefish_app.id)
21 |
22 | %h2 Invite a new team
23 |
24 | = form_for @admin, as: :admin, url: invite_teams_path(@admin), html: {method: :post} do |f|
25 | .control-group
26 | = f.label :email
27 | = f.text_field :email, placeholder: "Email"
28 |
29 | = f.submit "Send an invitation", class: "btn btn-primary"
30 |
--------------------------------------------------------------------------------
/app/views/deliveries/_from.html.haml:
--------------------------------------------------------------------------------
1 | = paginated_section deliveries, renderer: PagerRenderer, previous_label: image_tag("pager/previous.png", size: "13x14"), next_label: image_tag("pager/next.png", size: "13x14"), text: "Email" do
2 | %p.count= page_entries_info deliveries, model: "email"
3 | %table#deliveries.table.table-striped
4 | %tbody(data-provides="rowlink")
5 | - deliveries.each do |delivery|
6 | %tr
7 | %td.description
8 | = delivery.from
9 | %br
10 | = delivery.subject
11 | %td.app.hidden-phone= delivery.app.name
12 | %td.time-and-labels
13 | = link_to delivery_path(delivery.id), class: "rowlink" do
14 | = time_ago_in_words(delivery.created_at)
15 | ago
16 | %br
17 | = delivered_label(delivery.status)
18 | - if delivery.opened?
19 | %span.label.label-success Opened
20 | - if delivery.clicked?
21 | %span.label.label-success Clicked
22 |
--------------------------------------------------------------------------------
/app/views/deliveries/_click_events.html.haml:
--------------------------------------------------------------------------------
1 | %table.table.table-condensed
2 | %thead
3 | %tr
4 | %th Time
5 | %th Link
6 | %th Client
7 | %th OS
8 | %th IP
9 | %th Location
10 | %th
11 | %abbr{title: "Internet Service Provider"} ISP
12 | %th Org
13 | %tbody
14 | - delivery.click_events.each do |event|
15 | %tr
16 | %td
17 | = time_ago_in_words(event.created_at)
18 | ago
19 | %td= link_to event.url, event.url
20 | %td
21 | = event.user_agent.family
22 | - if event.user_agent.version
23 | %span.muted (#{event.user_agent.version})
24 | %td
25 | = event.os.family
26 | - if event.os.version
27 | %span.muted (#{event.os.version})
28 | %td= event.ip.address
29 | - if event.ip.info
30 | %td= "#{event.ip.info.city}, #{event.ip.info.region_name}, #{event.ip.info.country}"
31 | %td= event.ip.info.isp
32 | %td= event.ip.info.org
33 |
--------------------------------------------------------------------------------
/app/services/app_services/setup_custom_tracking_domain_ssl.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module AppServices
4 | # For an app with a valid custom_tracking_domain set generate an SSL certificate, setup our web server
5 | # use it and let the rest of the application to know to use it too
6 | class SetupCustomTrackingDomainSSL < ApplicationService
7 | def initialize(app:)
8 | super()
9 | @app = app
10 | end
11 |
12 | def call
13 | # Double check that we actually do have a custom tracking domain setup
14 | if app.custom_tracking_domain.blank?
15 | fail!
16 | return
17 | end
18 |
19 | # This will raise an exception if it fails for some reason
20 | Certificate.new(app.custom_tracking_domain).generate
21 |
22 | # So if we get here that means it worked
23 | if app.update(custom_tracking_domain_ssl_enabled: true)
24 | success!
25 | else
26 | fail!
27 | end
28 | end
29 |
30 | private
31 |
32 | attr_reader :app
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/app/views/deliveries/_to.html.haml:
--------------------------------------------------------------------------------
1 |
2 |
3 | = paginated_section deliveries, renderer: PagerRenderer, previous_label: image_tag("pager/previous.png", size: "13x14"), next_label: image_tag("pager/next.png", size: "13x14"), text: "Email" do
4 |
5 | %p.count= page_entries_info deliveries, model: "email"
6 | %table#deliveries.table.table-striped
7 | %tbody(data-provides="rowlink")
8 | - deliveries.each do |delivery|
9 | %tr
10 | %td.description
11 | = link_to delivery.to, delivery_path(delivery.id), class: "rowlink"
12 | %br
13 | = delivery.subject
14 | %td.app.hidden-phone= delivery.app.name
15 | %td.time-and-labels
16 | %span{title: delivery.created_at}= time_ago_in_words(delivery.created_at)
17 | ago
18 | %br
19 | = delivered_label(delivery.status)
20 | - if delivery.opened?
21 | %span.label.label-success Opened
22 | - if delivery.clicked?
23 | %span.label.label-success Clicked
24 |
--------------------------------------------------------------------------------
/config/secrets.yml:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Your secret key is used for verifying the integrity of signed cookies.
4 | # If you change this key, all old signed cookies will become invalid!
5 |
6 | # Make sure the secret is at least 30 characters and all random,
7 | # no regular words or you'll be exposed to dictionary attacks.
8 | # You can use `rake secret` to generate a secure secret key.
9 |
10 | # Make sure the secrets in this file are kept private
11 | # if you're sharing your code publicly.
12 |
13 | development:
14 | secret_key_base: 5b34e1d50356e780ced8f7dc53a55dc3620c627fa2b4180c0ac4f622c476b15da5587a055b4e0ae988bb5fdcc919f1f06ac0ec5b3834f85ce9e10251da92ed63
15 |
16 | test:
17 | secret_key_base: 336a709847e89bc76de5f4e570f4da0538d999128f062388054e3c587e636677ee75701c089a21e58ab3a322372c93fb021074137b8881c594a9c693368fbb36
18 |
19 | # Do not keep production secrets in the repository,
20 | # instead read values from the environment.
21 | production:
22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
23 |
--------------------------------------------------------------------------------
/lib/user_agent.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module UserAgent
4 | def calculate_ua_family
5 | parsed_user_agent.family
6 | end
7 |
8 | def calculate_ua_version
9 | v = parsed_user_agent.version
10 | v&.to_s
11 | end
12 |
13 | def calculate_os_family
14 | parsed_user_agent.os.family
15 | end
16 |
17 | def calculate_os_version
18 | v = parsed_user_agent.os.version
19 | v&.to_s
20 | end
21 |
22 | private
23 |
24 | def parsed_user_agent
25 | # @parsed_user_agent ||= user_agent_parser.parse(user_agent)
26 | user_agent_parser.parse(user_agent)
27 | end
28 |
29 | # Cache this between requests so that we don't keep reloading the
30 | # user agent database
31 | # TODO Put in a PR to the main project to update the default regexes
32 | # with the google image proxy
33 | # rubocop:disable Style/ClassVars
34 | def user_agent_parser
35 | @@user_agent_parser ||=
36 | UserAgentParser::Parser.new(patterns_path: "lib/regexes.yaml")
37 | end
38 | # rubocop:enable Style/ClassVars
39 | end
40 |
--------------------------------------------------------------------------------
/app/policies/app_policy.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AppPolicy < ApplicationPolicy
4 | def update?
5 | user &&
6 | user.team_id == record.team_id &&
7 | !Rails.configuration.cuttlefish_read_only_mode
8 | end
9 |
10 | def destroy?
11 | update?
12 | end
13 |
14 | def dkim?
15 | (
16 | user&.site_admin? &&
17 | record.cuttlefish? &&
18 | !Rails.configuration.cuttlefish_read_only_mode
19 | ) || update?
20 | end
21 |
22 | def webhook?
23 | dkim?
24 | end
25 |
26 | # TODO: No reason for this to be seperate from dkim above
27 | def toggle_dkim?
28 | dkim?
29 | end
30 |
31 | def upgrade_dkim?
32 | dkim?
33 | end
34 |
35 | def create?
36 | user && !Rails.configuration.cuttlefish_read_only_mode
37 | end
38 |
39 | def show?
40 | user&.site_admin? || super
41 | end
42 |
43 | class Scope < Scope
44 | def resolve
45 | if user
46 | scope.where(team_id: user.team_id)
47 | else
48 | scope.none
49 | end
50 | end
51 | end
52 | end
53 |
--------------------------------------------------------------------------------
/app/policies/delivery_policy.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class DeliveryPolicy < ApplicationPolicy
4 | def show?
5 | if user&.site_admin?
6 | true
7 | else
8 | app_ids = AppPolicy::Scope.new(user, App).resolve.pluck(:id)
9 | app_ids.include?(record.app_id)
10 | end
11 | end
12 |
13 | def create?
14 | user && !Rails.configuration.cuttlefish_read_only_mode
15 | end
16 |
17 | class Scope < Scope
18 | def resolve
19 | # If the user is a super admin they should have access to all the emails
20 | # However, they aren't shown by default in the admin UI because it only
21 | # lists the apps that the admin is attached to. To see the emails for an
22 | # app belonging to another team, they need to navigate via the teams list.
23 | if user&.site_admin?
24 | scope
25 | else
26 | # Avoid using join here as it was a lot slower
27 | app_ids = AppPolicy::Scope.new(user, App).resolve.pluck(:id)
28 | scope.where(app_id: app_ids)
29 | end
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/provisioning/roles/deploy-user/tasks/main.yml:
--------------------------------------------------------------------------------
1 | - name: Ensure we have a deploy group
2 | group:
3 | name: deploy
4 |
5 | - name: Ensure we have a deploy user
6 | user:
7 | name: deploy
8 | comment: "Deploy user"
9 | shell: /bin/bash
10 | group: deploy
11 |
12 | ## This will ensure that the listed keys are present
13 | ## but it does not remove other keys. If we have a need to revoke
14 | ## access we'll have to do something different.
15 | - name: Create authorized_keys files for local accounts
16 | authorized_key:
17 | user: "{{ item[0] }}"
18 | key: https://github.com/{{ item[1] }}.keys
19 | comment: "{{ item[1] }}"
20 | with_nested:
21 | #- ['deploy', 'ubuntu', 'root']
22 | - ['deploy', 'root']
23 | - "{{ github_users }}"
24 | register: add_authorized_keys
25 |
26 | - name: Only allow ssh access with keys
27 | lineinfile:
28 | dest: /etc/ssh/sshd_config
29 | state: present
30 | regexp: "^#?PasswordAuthentication"
31 | line: "PasswordAuthentication no"
32 | when: add_authorized_keys is success
33 | notify: sshd restart
34 |
--------------------------------------------------------------------------------