├── .dockerignore ├── test ├── keila │ ├── contacts │ │ ├── import_malformed2.csv │ │ ├── import_external_ids.csv │ │ ├── import_external_ids_duplicate.csv │ │ ├── import_malformed1.csv │ │ ├── import_with_data.csv │ │ ├── import_rfc_4180_upsert.csv │ │ └── import_rfc_4180_with_status.csv │ └── files │ │ ├── keila.jpg │ │ └── keila.png ├── test_helper.exs └── keila_web │ ├── views │ ├── layout_view_test.exs │ └── error_view_test.exs │ ├── helpers │ └── hcaptcha_test.exs │ └── controllers │ └── auth_controller_login_disabled_test.exs ├── .tool-versions ├── lib ├── keila_web │ ├── templates │ │ ├── public_form │ │ │ ├── success.html.heex │ │ │ ├── show.html.heex │ │ │ ├── unsubscribe.html.heex │ │ │ ├── double_opt_in_required.html.heex │ │ │ ├── double_opt_in_cancelled.html.heex │ │ │ └── double_opt_in_failure.html.heex │ │ ├── sender │ │ │ ├── _local_config.html.heex │ │ │ ├── _shared_local_config.html.heex │ │ │ ├── verification_failure.html.heex │ │ │ └── verification_success.html.heex │ │ ├── layout │ │ │ └── live.html.heex │ │ ├── auth │ │ │ ├── 404.html.heex │ │ │ ├── register_success.html.heex │ │ │ ├── activate_resend_success.html.heex │ │ │ ├── reset_success.html.heex │ │ │ ├── registration_disabled.html.heex │ │ │ ├── activate_success.html.heex │ │ │ └── reset_change_password_success.html.heex │ │ └── project │ │ │ └── _empty_state_campaigns.html.heex │ ├── views │ │ ├── auth_view.ex │ │ ├── account_view.ex │ │ ├── api_key_view.ex │ │ ├── project_view.ex │ │ ├── component_view.ex │ │ ├── instance_admin_view.ex │ │ ├── user_admin_view.ex │ │ ├── form_view.ex │ │ ├── public_form_layout_view.ex │ │ ├── shared_sender_admin_view.ex │ │ ├── error_view.ex │ │ ├── campaign_view.ex │ │ └── contact_view.ex │ ├── api │ │ ├── validators │ │ │ └── api_contact_filter_validator.ex │ │ ├── views │ │ │ ├── api_sender_view.ex │ │ │ └── api_segment_view.ex │ │ └── plugs │ │ │ └── api_authorization_plug.ex │ └── helpers │ │ ├── instance_info_plug.ex │ │ ├── meta │ │ └── meta_plug.ex │ │ ├── project │ │ └── project_plug.ex │ │ └── plain_text_json_parser │ │ └── plain_text_json_parser.ex └── keila │ ├── contacts │ └── import_error.ex │ ├── cldr.ex │ ├── instance │ ├── schemas │ │ └── instance.ex │ └── update_cron_worker.ex │ ├── pipe_helpers.ex │ ├── auth │ └── schemas │ │ ├── role_permission.ex │ │ ├── permission.ex │ │ └── role.ex │ ├── mailings │ ├── deliver_scheduled_campaigns_worker.ex │ ├── sender_adapters │ │ ├── local.ex │ │ ├── shared_local.ex │ │ └── sendgrid.ex │ └── schemas │ │ └── campaign_settings.ex │ ├── billing │ └── schemas │ │ └── plan.ex │ ├── accounts │ └── schemas │ │ ├── account.ex │ │ └── credit.ex │ └── tracking │ └── schemas │ ├── link.ex │ └── click.ex ├── assets ├── static │ ├── favicon.ico │ ├── images │ │ ├── pixel.gif │ │ └── og_image.png │ ├── fonts │ │ └── inter │ │ │ ├── Inter-Bold.woff │ │ │ ├── Inter-Thin.woff │ │ │ ├── Inter-Black.woff │ │ │ ├── Inter-Black.woff2 │ │ │ ├── Inter-Bold.woff2 │ │ │ ├── Inter-Italic.woff │ │ │ ├── Inter-Light.woff │ │ │ ├── Inter-Light.woff2 │ │ │ ├── Inter-Medium.woff │ │ │ ├── Inter-Thin.woff2 │ │ │ ├── Inter-ExtraBold.woff │ │ │ ├── Inter-Italic.woff2 │ │ │ ├── Inter-Medium.woff2 │ │ │ ├── Inter-Regular.woff │ │ │ ├── Inter-Regular.woff2 │ │ │ ├── Inter-SemiBold.woff │ │ │ ├── Inter-SemiBold.woff2 │ │ │ ├── Inter-BlackItalic.woff │ │ │ ├── Inter-BoldItalic.woff │ │ │ ├── Inter-BoldItalic.woff2 │ │ │ ├── Inter-ExtraBold.woff2 │ │ │ ├── Inter-ExtraLight.woff │ │ │ ├── Inter-ExtraLight.woff2 │ │ │ ├── Inter-LightItalic.woff │ │ │ ├── Inter-ThinItalic.woff │ │ │ ├── Inter-ThinItalic.woff2 │ │ │ ├── Inter-italic.var.woff2 │ │ │ ├── Inter-roman.var.woff2 │ │ │ ├── Inter-BlackItalic.woff2 │ │ │ ├── Inter-LightItalic.woff2 │ │ │ ├── Inter-MediumItalic.woff │ │ │ ├── Inter-MediumItalic.woff2 │ │ │ ├── Inter-SemiBoldItalic.woff │ │ │ ├── Inter-ExtraBoldItalic.woff │ │ │ ├── Inter-ExtraBoldItalic.woff2 │ │ │ ├── Inter-ExtraLightItalic.woff │ │ │ ├── Inter-SemiBoldItalic.woff2 │ │ │ └── Inter-ExtraLightItalic.woff2 │ ├── social-icons │ │ ├── xing-black.png │ │ ├── xing-white.png │ │ ├── github-black.png │ │ ├── github-white.png │ │ ├── reddit-black.png │ │ ├── reddit-white.png │ │ ├── signal-black.png │ │ ├── signal-white.png │ │ ├── slack-black.png │ │ ├── slack-white.png │ │ ├── tiktok-black.png │ │ ├── tiktok-white.png │ │ ├── twitch-black.png │ │ ├── twitch-white.png │ │ ├── bluesky-black.png │ │ ├── bluesky-white.png │ │ ├── discord-black.png │ │ ├── discord-white.png │ │ ├── discourse-black.png │ │ ├── discourse-white.png │ │ ├── facebook-black.png │ │ ├── facebook-white.png │ │ ├── instagram-black.png │ │ ├── instagram-white.png │ │ ├── linkedin-black.png │ │ ├── linkedin-white.png │ │ ├── mastodon-black.png │ │ ├── mastodon-white.png │ │ ├── snapchat-black.png │ │ ├── snapchat-white.png │ │ ├── threads-black.png │ │ ├── threads-white.png │ │ ├── whatsapp-black.png │ │ ├── whatsapp-white.png │ │ ├── x-twitter-black.png │ │ ├── x-twitter-white.png │ │ ├── youtube-black.png │ │ ├── youtube-white.png │ │ ├── soundcloud-black.png │ │ └── soundcloud-white.png │ ├── downloads │ │ ├── keila_import_template.ods │ │ └── keila_import_template.csv │ └── robots.txt ├── postcss.config.js ├── css │ ├── app.scss │ └── _header.scss ├── js │ ├── campaign-editors │ │ ├── markdown │ │ │ └── markdown-serializer.js │ │ └── block │ │ │ └── blocks │ │ │ └── separator.js │ └── hooks │ │ ├── campaign-settings-dialog.js │ │ ├── remember-unsaved.js │ │ └── file_manager.js └── tailwind.config.js ├── priv ├── repo │ └── migrations │ │ ├── .formatter.exs │ │ ├── 20220324085800_add_user_locale.exs │ │ ├── 20240524220616_add_mjml_body.exs │ │ ├── 20211101230000_add_contact_custom_data.exs │ │ ├── 20230111222000_add_campaign_json_body.exs │ │ ├── 20230308111500_add_campaign_preview_text.exs │ │ ├── 20210809112000_add_recipient_receipt.exs │ │ ├── 20230104115500_add_recipient_failed_at.exs │ │ ├── 20241227072034_add_parent_account.exs │ │ ├── 20210225161207_upgrade_oban_jobs_to_v10.exs │ │ ├── 20220825151500_upgrade_oban_jobs_to_v11.exs │ │ ├── 20231210140318_add_contact_double_opt_in_timestamp.exs │ │ ├── 20250407180538_add_user_name.exs │ │ ├── 20250513010703_add_campaign_public_link.exs │ │ ├── 20210105112000_projects.exs │ │ ├── 20250223090326_add_contact_external_id.exs │ │ ├── 20210104175000_root_group.exs │ │ ├── 20250407180947_add_account_data.exs │ │ ├── 20231203235509_add_form_double_opt_in_settings.exs │ │ ├── 20241117001430_create_instance.exs │ │ ├── 20210121100329_add_oban_jobs_table.exs │ │ ├── 20240809143953_add_recipient_queued_at.exs │ │ ├── 20210816142200_default_contact_status.exs │ │ ├── 20210428165000_shared_senders.exs │ │ ├── 20210222154306_add_deliver_at_to_campaigns.exs │ │ ├── 20220114154000_add_campaign_custom_data.exs │ │ ├── 20230120223000_add_segment_default_filter.exs │ │ ├── 20211116040700_segmentations.exs │ │ ├── 20210630205000_subscription_upserts.exs │ │ ├── 20250220063317_add_accounts_credit_transactions_on_delete.exs │ │ ├── 20210311225200_templates.exs │ │ ├── 20230604141200_add_contacts_form_params_table.exs │ │ ├── 20221227145500_add_recipient_states.exs │ │ ├── 20220523235000_add_files.exs │ │ ├── 20220905125000_emails_to_citext.exs │ │ ├── 20210319145300_form_timestamps.exs │ │ └── 20250707201249_add_verified_from_email_to_senders.exs ├── gettext │ ├── de │ │ └── LC_MESSAGES │ │ │ ├── auth.mo │ │ │ ├── cloud.mo │ │ │ ├── default.mo │ │ │ └── errors.mo │ └── fr │ │ └── LC_MESSAGES │ │ └── default.mo ├── email_templates │ ├── hybrid │ │ ├── _block--separator.liquid │ │ ├── _block--markdown.liquid │ │ ├── _block--paragraph.liquid │ │ ├── _block--heading.liquid │ │ ├── _block--list.liquid │ │ ├── _block--quote.liquid │ │ ├── _block--list_items.liquid │ │ └── _block--button.liquid │ └── default-text-content.txt └── vendor │ ├── hero-icons-outline │ ├── minus.svg │ ├── check.svg │ ├── minus-sm.svg │ ├── chevron-right.svg │ ├── chevron-up.svg │ ├── menu-alt-4.svg │ ├── x.svg │ ├── chevron-down.svg │ ├── chevron-left.svg │ ├── menu.svg │ ├── plus.svg │ ├── arrow-right.svg │ ├── arrow-up.svg │ ├── menu-alt-1.svg │ ├── menu-alt-2.svg │ ├── menu-alt-3.svg │ ├── plus-sm.svg │ ├── selector.svg │ ├── arrow-down.svg │ ├── arrow-left.svg │ ├── arrow-narrow-up.svg │ ├── lightning-bolt.svg │ ├── view-list.svg │ ├── arrow-narrow-down.svg │ ├── arrow-narrow-left.svg │ ├── arrow-narrow-right.svg │ ├── chevron-double-right.svg │ ├── chevron-double-up.svg │ ├── code.svg │ ├── hashtag.svg │ ├── reply.svg │ ├── trending-down.svg │ ├── trending-up.svg │ ├── chevron-double-down.svg │ ├── chevron-double-left.svg │ ├── clock.svg │ ├── paper-airplane.svg │ ├── pause.svg │ ├── search.svg │ ├── minus-circle.svg │ ├── shopping-bag.svg │ ├── bookmark.svg │ ├── check-circle.svg │ ├── sort-ascending.svg │ ├── sort-descending.svg │ ├── zoom-out.svg │ ├── switch-horizontal.svg │ ├── switch-vertical.svg │ ├── cube.svg │ ├── download.svg │ ├── exclamation-circle.svg │ ├── plus-circle.svg │ ├── upload.svg │ ├── arrow-circle-down.svg │ ├── arrow-circle-right.svg │ ├── arrow-circle-up.svg │ ├── arrow-uturn-left.svg │ ├── information-circle.svg │ ├── library.svg │ ├── user.svg │ ├── arrow-circle-left.svg │ ├── folder.svg │ ├── rss.svg │ ├── x-circle.svg │ ├── zoom-in.svg │ ├── currency-rupee.svg │ ├── currency-yen.svg │ ├── dots-circle-horizontal.svg │ ├── flag.svg │ ├── archive.svg │ ├── currency-pound.svg │ ├── device-mobile.svg │ ├── device-tablet.svg │ ├── external-link.svg │ ├── folder-remove.svg │ ├── moon.svg │ ├── calendar.svg │ ├── cloud.svg │ ├── emoji-happy.svg │ ├── emoji-sad.svg │ ├── folder-add.svg │ ├── logout.svg │ ├── save.svg │ ├── table.svg │ ├── terminal.svg │ ├── user-add.svg │ ├── user-remove.svg │ ├── ban.svg │ ├── bookmark-alt.svg │ ├── cloud-download.svg │ ├── cloud-upload.svg │ ├── credit-card.svg │ ├── login.svg │ ├── annotation.svg │ ├── arrows-expand.svg │ ├── chart-square-bar.svg │ ├── lock-open.svg │ ├── mail.svg │ ├── pencil.svg │ ├── presentation-chart-bar.svg │ ├── presentation-chart-line.svg │ ├── currency-bangladeshi.svg │ ├── folder-download.svg │ ├── receipt-refund.svg │ ├── search-circle.svg │ ├── chat-alt.svg │ ├── document.svg │ ├── lock-closed.svg │ ├── microphone.svg │ ├── refresh.svg │ ├── sparkles.svg │ ├── users.svg │ ├── at-symbol.svg │ ├── film.svg │ ├── ticket.svg │ ├── desktop-computer.svg │ ├── document-remove.svg │ ├── key.svg │ ├── document-add.svg │ ├── document-text.svg │ ├── dots-vertical.svg │ ├── duplicate.svg │ ├── heart.svg │ ├── paper-clip.svg │ ├── pencil-alt.svg │ ├── tag.svg │ ├── translate.svg │ ├── trash.svg │ ├── chip.svg │ ├── dots-horizontal.svg │ ├── folder-open.svg │ ├── link.svg │ ├── clipboard.svg │ ├── document-report.svg │ ├── user-circle.svg │ ├── wifi.svg │ ├── backspace.svg │ ├── cursor-click.svg │ ├── document-download.svg │ ├── exclamation.svg │ ├── gift.svg │ ├── newspaper.svg │ ├── video-camera.svg │ ├── adjustments.svg │ ├── cash.svg │ ├── database.svg │ ├── home.svg │ ├── scale.svg │ ├── status-online.svg │ ├── clipboard-check.svg │ ├── office-building.svg │ ├── save-as.svg │ ├── sun.svg │ ├── calculator.svg │ ├── collection.svg │ ├── fast-forward.svg │ ├── filter.svg │ ├── globe-alt.svg │ ├── music-note.svg │ ├── rewind.svg │ ├── server.svg │ ├── chat.svg │ ├── map.svg │ ├── photograph.svg │ ├── shopping-cart.svg │ ├── chat-alt-2.svg │ ├── clipboard-copy.svg │ ├── cube-transparent.svg │ ├── document-search.svg │ ├── support.svg │ ├── clipboard-list.svg │ ├── currency-euro.svg │ ├── hand.svg │ ├── question-mark-circle.svg │ ├── receipt-tax.svg │ ├── scissors.svg │ ├── document-duplicate.svg │ ├── printer.svg │ ├── briefcase.svg │ ├── chart-pie.svg │ ├── currency-dollar.svg │ ├── stop.svg │ ├── bell.svg │ ├── volume-up.svg │ ├── inbox.svg │ ├── chart-bar.svg │ ├── color-swatch.svg │ ├── globe.svg │ ├── shield-check.svg │ ├── template.svg │ ├── speakerphone.svg │ ├── view-boards.svg │ ├── exclamation-triangle.svg │ ├── inbox-in.svg │ ├── location-marker.svg │ ├── shield-exclamation.svg │ ├── light-bulb.svg │ ├── mail-open.svg │ ├── phone.svg │ ├── play.svg │ ├── identification.svg │ ├── view-grid-add.svg │ ├── thumb-down.svg │ ├── share.svg │ ├── eye.svg │ ├── thumb-up.svg │ ├── variable.svg │ ├── puzzle.svg │ ├── book-open.svg │ ├── phone-incoming.svg │ ├── phone-outgoing.svg │ ├── beaker.svg │ ├── phone-missed-call.svg │ ├── view-grid.svg │ ├── qrcode.svg │ ├── status-offline.svg │ ├── user-group.svg │ ├── volume-off.svg │ ├── camera.svg │ ├── finger-print.svg │ ├── cake.svg │ ├── fire.svg │ ├── eye-off.svg │ ├── truck.svg │ ├── star.svg │ ├── academic-cap.svg │ └── badge-check.svg │ ├── keila-icons │ ├── horizontal_rule.svg │ ├── indent-decrease.svg │ ├── indent-increase.svg │ ├── button-alt.svg │ ├── list-unordered.svg │ ├── block.svg │ ├── logo.svg │ └── mjml.svg │ ├── social-icons │ ├── x-twitter.svg │ ├── twitch.svg │ ├── tiktok.svg │ ├── discourse.svg │ ├── facebook.svg │ ├── youtube.svg │ ├── xing.svg │ ├── bluesky.svg │ └── linkedin.svg │ └── font-awesome │ ├── spinner.svg │ ├── align-left.svg │ ├── align-justify.svg │ ├── align-right.svg │ └── align-center.svg ├── .github └── assets │ ├── keila-20210705.jpg │ ├── keila-20211203.jpg │ ├── devcontainer-button.png │ └── logo.svg ├── extra ├── keila │ └── license │ │ └── props.ex ├── license.public.pem ├── keila_cloud │ ├── config │ │ └── cloud_config.exs │ └── schemas │ │ └── account.ex └── keila_cloud_web │ ├── views │ ├── cloud_admin_view.ex │ └── cloud_sender_view.ex │ ├── controllers │ └── cloud_account_controller.ex │ └── templates │ └── cloud_account │ └── _completed.html.heex ├── .formatter.exs ├── .zed └── settings.json └── dprint.json /.dockerignore: -------------------------------------------------------------------------------- 1 | _build 2 | deps 3 | assets/node_modules 4 | priv/static -------------------------------------------------------------------------------- /test/keila/contacts/import_malformed2.csv: -------------------------------------------------------------------------------- 1 | Email, First name, Last name 2 | ,Foo,Bar -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | elixir 1.18.4-otp-27 2 | erlang 27.3 3 | dprint 0.50.0 4 | nodejs 20.19.2 5 | -------------------------------------------------------------------------------- /lib/keila_web/templates/public_form/success.html.heex: -------------------------------------------------------------------------------- 1 | <%= render_form_success(@form) %> 2 | -------------------------------------------------------------------------------- /assets/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/favicon.ico -------------------------------------------------------------------------------- /lib/keila_web/templates/public_form/show.html.heex: -------------------------------------------------------------------------------- 1 | <%= render_form(@form, @changeset, @mode) %> 2 | -------------------------------------------------------------------------------- /lib/keila_web/templates/public_form/unsubscribe.html.heex: -------------------------------------------------------------------------------- 1 | <%= render_unsubscribe_form(@form) %> 2 | -------------------------------------------------------------------------------- /lib/keila_web/views/auth_view.ex: -------------------------------------------------------------------------------- 1 | defmodule KeilaWeb.AuthView do 2 | use KeilaWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /priv/repo/migrations/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:ecto_sql], 3 | inputs: ["*.exs"] 4 | ] 5 | -------------------------------------------------------------------------------- /test/keila/files/keila.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/test/keila/files/keila.jpg -------------------------------------------------------------------------------- /test/keila/files/keila.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/test/keila/files/keila.png -------------------------------------------------------------------------------- /lib/keila_web/views/account_view.ex: -------------------------------------------------------------------------------- 1 | defmodule KeilaWeb.AccountView do 2 | use KeilaWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /lib/keila_web/views/api_key_view.ex: -------------------------------------------------------------------------------- 1 | defmodule KeilaWeb.ApiKeyView do 2 | use KeilaWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /lib/keila_web/views/project_view.ex: -------------------------------------------------------------------------------- 1 | defmodule KeilaWeb.ProjectView do 2 | use KeilaWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /assets/static/images/pixel.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/images/pixel.gif -------------------------------------------------------------------------------- /lib/keila_web/views/component_view.ex: -------------------------------------------------------------------------------- 1 | defmodule KeilaWeb.ComponentView do 2 | use KeilaWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /.github/assets/keila-20210705.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/.github/assets/keila-20210705.jpg -------------------------------------------------------------------------------- /.github/assets/keila-20211203.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/.github/assets/keila-20211203.jpg -------------------------------------------------------------------------------- /assets/static/images/og_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/images/og_image.png -------------------------------------------------------------------------------- /extra/keila/license/props.ex: -------------------------------------------------------------------------------- 1 | defmodule Keila.License.Props do 2 | defstruct [:name, :email, :expires, :type] 3 | end 4 | -------------------------------------------------------------------------------- /lib/keila_web/views/instance_admin_view.ex: -------------------------------------------------------------------------------- 1 | defmodule KeilaWeb.InstanceAdminView do 2 | use KeilaWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /priv/gettext/de/LC_MESSAGES/auth.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/priv/gettext/de/LC_MESSAGES/auth.mo -------------------------------------------------------------------------------- /priv/gettext/de/LC_MESSAGES/cloud.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/priv/gettext/de/LC_MESSAGES/cloud.mo -------------------------------------------------------------------------------- /.github/assets/devcontainer-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/.github/assets/devcontainer-button.png -------------------------------------------------------------------------------- /priv/gettext/de/LC_MESSAGES/default.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/priv/gettext/de/LC_MESSAGES/default.mo -------------------------------------------------------------------------------- /priv/gettext/de/LC_MESSAGES/errors.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/priv/gettext/de/LC_MESSAGES/errors.mo -------------------------------------------------------------------------------- /priv/gettext/fr/LC_MESSAGES/default.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/priv/gettext/fr/LC_MESSAGES/default.mo -------------------------------------------------------------------------------- /assets/static/fonts/inter/Inter-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/fonts/inter/Inter-Bold.woff -------------------------------------------------------------------------------- /assets/static/fonts/inter/Inter-Thin.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/fonts/inter/Inter-Thin.woff -------------------------------------------------------------------------------- /assets/static/social-icons/xing-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/social-icons/xing-black.png -------------------------------------------------------------------------------- /assets/static/social-icons/xing-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/social-icons/xing-white.png -------------------------------------------------------------------------------- /lib/keila/contacts/import_error.ex: -------------------------------------------------------------------------------- 1 | defmodule Keila.Contacts.ImportError do 2 | defexception [:message, :line, :column] 3 | end 4 | -------------------------------------------------------------------------------- /lib/keila_web/templates/public_form/double_opt_in_required.html.heex: -------------------------------------------------------------------------------- 1 | <%= render_form_double_opt_in_required(@form, @email) %> 2 | -------------------------------------------------------------------------------- /lib/keila_web/views/user_admin_view.ex: -------------------------------------------------------------------------------- 1 | defmodule KeilaWeb.UserAdminView do 2 | use KeilaWeb, :view 3 | require Keila 4 | end 5 | -------------------------------------------------------------------------------- /assets/static/fonts/inter/Inter-Black.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/fonts/inter/Inter-Black.woff -------------------------------------------------------------------------------- /assets/static/fonts/inter/Inter-Black.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/fonts/inter/Inter-Black.woff2 -------------------------------------------------------------------------------- /assets/static/fonts/inter/Inter-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/fonts/inter/Inter-Bold.woff2 -------------------------------------------------------------------------------- /assets/static/fonts/inter/Inter-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/fonts/inter/Inter-Italic.woff -------------------------------------------------------------------------------- /assets/static/fonts/inter/Inter-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/fonts/inter/Inter-Light.woff -------------------------------------------------------------------------------- /assets/static/fonts/inter/Inter-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/fonts/inter/Inter-Light.woff2 -------------------------------------------------------------------------------- /assets/static/fonts/inter/Inter-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/fonts/inter/Inter-Medium.woff -------------------------------------------------------------------------------- /assets/static/fonts/inter/Inter-Thin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/fonts/inter/Inter-Thin.woff2 -------------------------------------------------------------------------------- /assets/static/social-icons/github-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/social-icons/github-black.png -------------------------------------------------------------------------------- /assets/static/social-icons/github-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/social-icons/github-white.png -------------------------------------------------------------------------------- /assets/static/social-icons/reddit-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/social-icons/reddit-black.png -------------------------------------------------------------------------------- /assets/static/social-icons/reddit-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/social-icons/reddit-white.png -------------------------------------------------------------------------------- /assets/static/social-icons/signal-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/social-icons/signal-black.png -------------------------------------------------------------------------------- /assets/static/social-icons/signal-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/social-icons/signal-white.png -------------------------------------------------------------------------------- /assets/static/social-icons/slack-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/social-icons/slack-black.png -------------------------------------------------------------------------------- /assets/static/social-icons/slack-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/social-icons/slack-white.png -------------------------------------------------------------------------------- /assets/static/social-icons/tiktok-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/social-icons/tiktok-black.png -------------------------------------------------------------------------------- /assets/static/social-icons/tiktok-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/social-icons/tiktok-white.png -------------------------------------------------------------------------------- /assets/static/social-icons/twitch-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/social-icons/twitch-black.png -------------------------------------------------------------------------------- /assets/static/social-icons/twitch-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/social-icons/twitch-white.png -------------------------------------------------------------------------------- /assets/static/fonts/inter/Inter-ExtraBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/fonts/inter/Inter-ExtraBold.woff -------------------------------------------------------------------------------- /assets/static/fonts/inter/Inter-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/fonts/inter/Inter-Italic.woff2 -------------------------------------------------------------------------------- /assets/static/fonts/inter/Inter-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/fonts/inter/Inter-Medium.woff2 -------------------------------------------------------------------------------- /assets/static/fonts/inter/Inter-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/fonts/inter/Inter-Regular.woff -------------------------------------------------------------------------------- /assets/static/fonts/inter/Inter-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/fonts/inter/Inter-Regular.woff2 -------------------------------------------------------------------------------- /assets/static/fonts/inter/Inter-SemiBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/fonts/inter/Inter-SemiBold.woff -------------------------------------------------------------------------------- /assets/static/fonts/inter/Inter-SemiBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/fonts/inter/Inter-SemiBold.woff2 -------------------------------------------------------------------------------- /assets/static/social-icons/bluesky-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/social-icons/bluesky-black.png -------------------------------------------------------------------------------- /assets/static/social-icons/bluesky-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/social-icons/bluesky-white.png -------------------------------------------------------------------------------- /assets/static/social-icons/discord-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/social-icons/discord-black.png -------------------------------------------------------------------------------- /assets/static/social-icons/discord-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/social-icons/discord-white.png -------------------------------------------------------------------------------- /assets/static/social-icons/discourse-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/social-icons/discourse-black.png -------------------------------------------------------------------------------- /assets/static/social-icons/discourse-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/social-icons/discourse-white.png -------------------------------------------------------------------------------- /assets/static/social-icons/facebook-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/social-icons/facebook-black.png -------------------------------------------------------------------------------- /assets/static/social-icons/facebook-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/social-icons/facebook-white.png -------------------------------------------------------------------------------- /assets/static/social-icons/instagram-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/social-icons/instagram-black.png -------------------------------------------------------------------------------- /assets/static/social-icons/instagram-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/social-icons/instagram-white.png -------------------------------------------------------------------------------- /assets/static/social-icons/linkedin-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/social-icons/linkedin-black.png -------------------------------------------------------------------------------- /assets/static/social-icons/linkedin-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/social-icons/linkedin-white.png -------------------------------------------------------------------------------- /assets/static/social-icons/mastodon-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/social-icons/mastodon-black.png -------------------------------------------------------------------------------- /assets/static/social-icons/mastodon-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/social-icons/mastodon-white.png -------------------------------------------------------------------------------- /assets/static/social-icons/snapchat-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/social-icons/snapchat-black.png -------------------------------------------------------------------------------- /assets/static/social-icons/snapchat-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/social-icons/snapchat-white.png -------------------------------------------------------------------------------- /assets/static/social-icons/threads-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/social-icons/threads-black.png -------------------------------------------------------------------------------- /assets/static/social-icons/threads-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/social-icons/threads-white.png -------------------------------------------------------------------------------- /assets/static/social-icons/whatsapp-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/social-icons/whatsapp-black.png -------------------------------------------------------------------------------- /assets/static/social-icons/whatsapp-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/social-icons/whatsapp-white.png -------------------------------------------------------------------------------- /assets/static/social-icons/x-twitter-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/social-icons/x-twitter-black.png -------------------------------------------------------------------------------- /assets/static/social-icons/x-twitter-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/social-icons/x-twitter-white.png -------------------------------------------------------------------------------- /assets/static/social-icons/youtube-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/social-icons/youtube-black.png -------------------------------------------------------------------------------- /assets/static/social-icons/youtube-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/social-icons/youtube-white.png -------------------------------------------------------------------------------- /assets/static/fonts/inter/Inter-BlackItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/fonts/inter/Inter-BlackItalic.woff -------------------------------------------------------------------------------- /assets/static/fonts/inter/Inter-BoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/fonts/inter/Inter-BoldItalic.woff -------------------------------------------------------------------------------- /assets/static/fonts/inter/Inter-BoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/fonts/inter/Inter-BoldItalic.woff2 -------------------------------------------------------------------------------- /assets/static/fonts/inter/Inter-ExtraBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/fonts/inter/Inter-ExtraBold.woff2 -------------------------------------------------------------------------------- /assets/static/fonts/inter/Inter-ExtraLight.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/fonts/inter/Inter-ExtraLight.woff -------------------------------------------------------------------------------- /assets/static/fonts/inter/Inter-ExtraLight.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/fonts/inter/Inter-ExtraLight.woff2 -------------------------------------------------------------------------------- /assets/static/fonts/inter/Inter-LightItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/fonts/inter/Inter-LightItalic.woff -------------------------------------------------------------------------------- /assets/static/fonts/inter/Inter-ThinItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/fonts/inter/Inter-ThinItalic.woff -------------------------------------------------------------------------------- /assets/static/fonts/inter/Inter-ThinItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/fonts/inter/Inter-ThinItalic.woff2 -------------------------------------------------------------------------------- /assets/static/fonts/inter/Inter-italic.var.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/fonts/inter/Inter-italic.var.woff2 -------------------------------------------------------------------------------- /assets/static/fonts/inter/Inter-roman.var.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/fonts/inter/Inter-roman.var.woff2 -------------------------------------------------------------------------------- /assets/static/social-icons/soundcloud-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/social-icons/soundcloud-black.png -------------------------------------------------------------------------------- /assets/static/social-icons/soundcloud-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/social-icons/soundcloud-white.png -------------------------------------------------------------------------------- /priv/email_templates/hybrid/_block--separator.liquid: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | -------------------------------------------------------------------------------- /assets/static/downloads/keila_import_template.ods: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/downloads/keila_import_template.ods -------------------------------------------------------------------------------- /assets/static/fonts/inter/Inter-BlackItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/fonts/inter/Inter-BlackItalic.woff2 -------------------------------------------------------------------------------- /assets/static/fonts/inter/Inter-LightItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/fonts/inter/Inter-LightItalic.woff2 -------------------------------------------------------------------------------- /assets/static/fonts/inter/Inter-MediumItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/fonts/inter/Inter-MediumItalic.woff -------------------------------------------------------------------------------- /assets/static/fonts/inter/Inter-MediumItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/fonts/inter/Inter-MediumItalic.woff2 -------------------------------------------------------------------------------- /assets/static/fonts/inter/Inter-SemiBoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/fonts/inter/Inter-SemiBoldItalic.woff -------------------------------------------------------------------------------- /extra/license.public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MCowBQYDK2VwAyEAZGDDgaezSrOe6kGtdxls7LH2gb+rF9Hnd8RKjFUxNtM= 3 | -----END PUBLIC KEY----- 4 | -------------------------------------------------------------------------------- /assets/static/fonts/inter/Inter-ExtraBoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/fonts/inter/Inter-ExtraBoldItalic.woff -------------------------------------------------------------------------------- /assets/static/fonts/inter/Inter-ExtraBoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/fonts/inter/Inter-ExtraBoldItalic.woff2 -------------------------------------------------------------------------------- /assets/static/fonts/inter/Inter-ExtraLightItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/fonts/inter/Inter-ExtraLightItalic.woff -------------------------------------------------------------------------------- /assets/static/fonts/inter/Inter-SemiBoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/fonts/inter/Inter-SemiBoldItalic.woff2 -------------------------------------------------------------------------------- /priv/email_templates/hybrid/_block--markdown.liquid: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ block.data }} 4 | 5 | 6 | -------------------------------------------------------------------------------- /test/keila/contacts/import_external_ids.csv: -------------------------------------------------------------------------------- 1 | Email,External ID 2 | foo@example.com, 3 | foo@example.com,1 4 | foo2@example.com,1 5 | foo3@example.com,3 6 | -------------------------------------------------------------------------------- /assets/static/fonts/inter/Inter-ExtraLightItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pentacent/keila/HEAD/assets/static/fonts/inter/Inter-ExtraLightItalic.woff2 -------------------------------------------------------------------------------- /test/keila/contacts/import_external_ids_duplicate.csv: -------------------------------------------------------------------------------- 1 | Email,External ID 2 | foo@example.com, 3 | foo@example.com,1 4 | foo2@example.com 5 | foo2@example.com,1 6 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | Keila.Factory.start_counter() 2 | Ecto.Adapters.SQL.Sandbox.mode(Keila.Repo, :manual) 3 | ExUnit.configure(exclude: :skip) 4 | ExUnit.start() 5 | -------------------------------------------------------------------------------- /lib/keila_web/views/form_view.ex: -------------------------------------------------------------------------------- 1 | defmodule KeilaWeb.FormView do 2 | use KeilaWeb, :view 3 | import KeilaWeb.PublicFormLayoutView, only: [build_styles: 1] 4 | end 5 | -------------------------------------------------------------------------------- /priv/email_templates/hybrid/_block--paragraph.liquid: -------------------------------------------------------------------------------- 1 | 2 | 3 |

{{block.data.text }}

4 | 5 | 6 | -------------------------------------------------------------------------------- /test/keila/contacts/import_malformed1.csv: -------------------------------------------------------------------------------- 1 | Email, First name, Last name 2 | foo@example.com, Foo, Bar 3 | "foo2@example.com","Foo2","Bar2" 4 | foo3@example.com, "Foo 3", "Bar3" 5 | -------------------------------------------------------------------------------- /test/keila/contacts/import_with_data.csv: -------------------------------------------------------------------------------- 1 | Email,First name,Last name,Data 2 | foo-1@example.com,Joël,Müller-Schultheiß,"{""foo"":""bar""}" 3 | foo-2@example.com,Eliška,Þorláksson,"{""foo"":[1,2,3]}" 4 | -------------------------------------------------------------------------------- /priv/email_templates/hybrid/_block--heading.liquid: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ block.data.text }} 4 | 5 | 6 | -------------------------------------------------------------------------------- /priv/email_templates/hybrid/_block--list.liquid: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% render "block--list_items", style: block.data.style, items: block.data.items %} 4 | 5 | 6 | -------------------------------------------------------------------------------- /test/keila/contacts/import_rfc_4180_upsert.csv: -------------------------------------------------------------------------------- 1 | Email,First name,Last name 2 | joao@example.com,João,Milton 3 | elisa@example.com,Elisa,Paula 4 | foo-1@example.com,Foo,Bar 5 | foo-2@example.com,Foo,Bar 6 | joao@example.com,João,Nilton -------------------------------------------------------------------------------- /lib/keila/cldr.ex: -------------------------------------------------------------------------------- 1 | defmodule Keila.Cldr do 2 | use Cldr, 3 | otp_app: :keila, 4 | locales: Application.compile_env(:keila, KeilaWeb.Gettext)[:locales], 5 | default_locale: "en", 6 | providers: [Cldr.Territory] 7 | end 8 | -------------------------------------------------------------------------------- /lib/keila/instance/schemas/instance.ex: -------------------------------------------------------------------------------- 1 | defmodule Keila.Instance.Instance do 2 | use Keila.Schema 3 | 4 | schema "instance" do 5 | embeds_many :available_updates, Keila.Instance.Release, on_replace: :delete 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/minus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /c/ 3 | Disallow: /r/ 4 | Disallow: /forms/ 5 | Disallow: /verify-sender/ 6 | Disallow: /double-opt-in/ 7 | Disallow: /unsubscribe/ 8 | Disallow: /uploads/ 9 | Disallow: /archive/ 10 | -------------------------------------------------------------------------------- /lib/keila/pipe_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule Keila.PipeHelpers do 2 | def tap_if_not_nil(value, fun) 3 | def tap_if_not_nil(nil, _), do: nil 4 | 5 | def tap_if_not_nil(value, fun) do 6 | fun.(value) 7 | value 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/minus-sm.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/keila-icons/horizontal_rule.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/chevron-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/chevron-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/menu-alt-4.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | plugins: [Phoenix.LiveView.HTMLFormatter], 3 | import_deps: [:ecto, :phoenix], 4 | inputs: ["*.{heex,ex,exs}", "priv/*/seeds.exs", "{config,lib,test,extra}/**/*.{heex,ex,exs}"], 5 | subdirectories: ["priv/*/migrations"] 6 | ] 7 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/chevron-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/chevron-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /test/keila/contacts/import_rfc_4180_with_status.csv: -------------------------------------------------------------------------------- 1 | Email,First name,Last name,Status 2 | active@example.com,Foo,Bar,active 3 | unsubscribed@example.com,Foo,Bar,unsubscribed 4 | unreachable@example.com,Foo,Bar,unreachable 5 | empty@example.com,Foo,Bar, 6 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/arrow-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/arrow-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/menu-alt-1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/menu-alt-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/menu-alt-3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/plus-sm.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/selector.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/arrow-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/arrow-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/arrow-narrow-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/lightning-bolt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/view-list.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/repo/migrations/20220324085800_add_user_locale.exs: -------------------------------------------------------------------------------- 1 | defmodule Keila.Repo.Migrations.AddUserLocale do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table("users") do 6 | add :locale, :string, size: 6 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20240524220616_add_mjml_body.exs: -------------------------------------------------------------------------------- 1 | defmodule Keila.Repo.Migrations.AddMjmlBody do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table("mailings_campaigns") do 6 | add :mjml_body, :text 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/arrow-narrow-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/arrow-narrow-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/arrow-narrow-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/chevron-double-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/chevron-double-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/code.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/hashtag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/reply.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/trending-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/trending-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /extra/keila_cloud/config/cloud_config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | adapters = read_config(:keila)[Keila.Mailings.SenderAdapters][:adapters] 4 | 5 | config :keila, Keila.Mailings.SenderAdapters, 6 | adapters: [ 7 | KeilaCloud.Mailings.SendWithKeila | adapters 8 | ] 9 | -------------------------------------------------------------------------------- /priv/repo/migrations/20211101230000_add_contact_custom_data.exs: -------------------------------------------------------------------------------- 1 | defmodule Keila.Repo.Migrations.AddContactCustomData do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table("contacts") do 6 | add :data, :jsonb 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/chevron-double-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/chevron-double-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/clock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/paper-airplane.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/pause.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | syntax: "postcss-scss", 3 | plugins: [ 4 | require("postcss-import"), 5 | require("tailwindcss/nesting"), 6 | require("tailwindcss")("./tailwind.config.js"), 7 | require("autoprefixer") 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/minus-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/shopping-bag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/keila-icons/indent-decrease.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/keila-icons/indent-increase.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/repo/migrations/20230111222000_add_campaign_json_body.exs: -------------------------------------------------------------------------------- 1 | defmodule Keila.Repo.Migrations.AddCampaignJsonBody do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table("mailings_campaigns") do 6 | add :json_body, :json 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/bookmark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/check-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/sort-ascending.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/sort-descending.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/zoom-out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/email_templates/hybrid/_block--quote.liquid: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
{{block.data.text }}
5 |
{{ block.data.caption }}
6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/switch-horizontal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/switch-vertical.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/repo/migrations/20230308111500_add_campaign_preview_text.exs: -------------------------------------------------------------------------------- 1 | defmodule Keila.Repo.Migrations.AddCampaignPreviewText do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table("mailings_campaigns") do 6 | add :preview_text, :text 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/cube.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/download.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/exclamation-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/plus-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/upload.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /lib/keila_web/templates/sender/_local_config.html.heex: -------------------------------------------------------------------------------- 1 |
2 | <%= gettext( 3 | "The local sender adapter is useful for testing during development. Inspect its mailbox at: %{url}", 4 | url: Routes.static_url(KeilaWeb.Endpoint, "/dev/mailbox") 5 | ) %> 6 |
7 | -------------------------------------------------------------------------------- /priv/repo/migrations/20210809112000_add_recipient_receipt.exs: -------------------------------------------------------------------------------- 1 | defmodule Keila.Repo.Migrations.AddRecipeintReceipt do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table("mailings_recipients") do 6 | add :receipt, :string, length: 32 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20230104115500_add_recipient_failed_at.exs: -------------------------------------------------------------------------------- 1 | defmodule Keila.Repo.Migrations.AddRecipientFailedAt do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table("mailings_recipients") do 6 | add :failed_at, :utc_datetime 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/arrow-circle-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/arrow-circle-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/arrow-circle-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/arrow-uturn-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/information-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/library.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/user.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/arrow-circle-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/folder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/rss.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/x-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/zoom-in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/keila-icons/button-alt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /priv/repo/migrations/20241227072034_add_parent_account.exs: -------------------------------------------------------------------------------- 1 | defmodule Keila.Repo.Migrations.AddParentAccount do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:accounts) do 6 | add :parent_id, references("accounts", on_delete: :nilify_all) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/currency-rupee.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/currency-yen.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/dots-circle-horizontal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/flag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/repo/migrations/20210225161207_upgrade_oban_jobs_to_v10.exs: -------------------------------------------------------------------------------- 1 | defmodule Keila.Repo.Migrations.UpgradeObanJobsToV10 do 2 | use Ecto.Migration 3 | 4 | def up do 5 | Oban.Migrations.up(version: 10) 6 | end 7 | 8 | def down do 9 | Oban.Migrations.down(version: 10) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /priv/repo/migrations/20220825151500_upgrade_oban_jobs_to_v11.exs: -------------------------------------------------------------------------------- 1 | defmodule Keila.Repo.Migrations.UpgradeObanJobsToV11 do 2 | use Ecto.Migration 3 | 4 | def up do 5 | Oban.Migrations.up(version: 11) 6 | end 7 | 8 | def down do 9 | Oban.Migrations.down(version: 11) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /priv/repo/migrations/20231210140318_add_contact_double_opt_in_timestamp.exs: -------------------------------------------------------------------------------- 1 | defmodule Keila.Repo.Migrations.AddContactDoubleOptInTimestamp do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table("contacts") do 6 | add :double_opt_in_at, :utc_datetime 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20250407180538_add_user_name.exs: -------------------------------------------------------------------------------- 1 | defmodule Keila.Repo.Migrations.AddUserName do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:users) do 6 | add :given_name, :string, size: 64 7 | add :family_name, :string, size: 64 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/archive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/currency-pound.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/device-mobile.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/device-tablet.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/external-link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/folder-remove.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/moon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/css/app.scss: -------------------------------------------------------------------------------- 1 | /* 2 | This is the main entrypoint for Tailwind/PostCSS. 3 | */ 4 | 5 | @import "tailwindcss/base"; 6 | @import "tailwindcss/components"; 7 | @import "tailwindcss/utilities"; 8 | 9 | @import "./phoenix.css"; 10 | 11 | @import "./styles.scss"; 12 | @import "./_markdown_wysiwyg.scss"; 13 | -------------------------------------------------------------------------------- /assets/js/campaign-editors/markdown/markdown-serializer.js: -------------------------------------------------------------------------------- 1 | import { defaultMarkdownSerializer } from "prosemirror-markdown" 2 | 3 | const markdownSerializer = defaultMarkdownSerializer 4 | markdownSerializer.marks.liquid = { open: "", close: "", mixable: false, escape: false } 5 | 6 | export { markdownSerializer } 7 | -------------------------------------------------------------------------------- /priv/repo/migrations/20250513010703_add_campaign_public_link.exs: -------------------------------------------------------------------------------- 1 | defmodule Keila.Repo.Migrations.AddCampaignPublicLink do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table("mailings_campaigns") do 6 | add :public_link_enabled, :boolean, default: false 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/calendar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/cloud.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/emoji-happy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/emoji-sad.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/folder-add.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/logout.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/save.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/table.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/terminal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/user-add.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/user-remove.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/ban.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/bookmark-alt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/cloud-download.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/cloud-upload.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/credit-card.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/login.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/static/downloads/keila_import_template.csv: -------------------------------------------------------------------------------- 1 | Email,First name,Last name,Data,External ID 2 | jane@example.com,Jane,Doe,"{""marketing_consent"": true, ""age"": 34, ""birthplace"": ""Paris"", ""tags"":[""rocket-scientist""]}", 3 | john@example.com,John,Smith,"{""birthplace"": ""Berlin"", ""tags"":[""book-enthusiast""]}", 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/annotation.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/arrows-expand.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/chart-square-bar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/lock-open.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/mail.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/pencil.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/presentation-chart-bar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/presentation-chart-line.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/currency-bangladeshi.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/folder-download.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/receipt-refund.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/search-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/chat-alt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/document.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/lock-closed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/microphone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/refresh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/sparkles.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/users.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /test/keila_web/views/layout_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule KeilaWeb.LayoutViewTest do 2 | use KeilaWeb.ConnCase, async: true 3 | 4 | # When testing helpers, you may want to import Phoenix.HTML and 5 | # use functions such as safe_to_string() to convert the helper 6 | # result into an HTML string. 7 | # import Phoenix.HTML 8 | end 9 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/at-symbol.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/film.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/ticket.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /lib/keila_web/templates/public_form/double_opt_in_cancelled.html.heex: -------------------------------------------------------------------------------- 1 |
2 | <%= render_h1(@form) %> 3 |

4 | <%= gettext("Your data has been deleted.") %> 5 |

6 |

7 | <%= gettext("You will not be subscribed to this list.") %> 8 |

9 | <%= render_fine_print(@form) %> 10 |
11 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/desktop-computer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/document-remove.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/key.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /lib/keila_web/templates/sender/_shared_local_config.html.heex: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 | <%= render_icon(:check_circle) %> 5 | 6 | <%= gettext("No configuration is required for this sender.") %> 7 |

8 |
9 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/document-add.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/document-text.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/dots-vertical.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/duplicate.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/heart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/paper-clip.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/pencil-alt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/tag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/translate.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/trash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/repo/migrations/20210105112000_projects.exs: -------------------------------------------------------------------------------- 1 | defmodule Keila.Repo.Migrations.Projects do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table("projects") do 6 | add :group_id, references("groups", on_delete: :delete_all, null: false) 7 | add :name, :string 8 | timestamps() 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/chip.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/dots-horizontal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/folder-open.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/js/hooks/campaign-settings-dialog.js: -------------------------------------------------------------------------------- 1 | const CampaignSettingsDialogHook = { 2 | mounted() { 3 | this.handleEvent("settings_validated", ({ valid }) => { 4 | if (valid) { 5 | this.el.dispatchEvent(new CustomEvent("x-confirm", { detail: {} })) 6 | } 7 | }) 8 | } 9 | } 10 | 11 | export { CampaignSettingsDialogHook } 12 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/clipboard.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/document-report.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/user-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/wifi.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/repo/migrations/20250223090326_add_contact_external_id.exs: -------------------------------------------------------------------------------- 1 | defmodule Keila.Repo.Migrations.AddContactExternalId do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table("contacts") do 6 | add :external_id, :string, size: 40 7 | end 8 | 9 | create unique_index("contacts", [:external_id, :project_id]) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/backspace.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/cursor-click.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/document-download.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/exclamation.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/gift.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/newspaper.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/video-camera.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/adjustments.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/cash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/database.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/home.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/scale.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/status-online.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /lib/keila_web/templates/layout/live.html.heex: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | <%= @inner_content %> 10 | -------------------------------------------------------------------------------- /priv/email_templates/default-text-content.txt: -------------------------------------------------------------------------------- 1 | Hey {{ contact.first_name | default: "there" }}! 2 | 3 | You can write your campaign text here. This is a plain-text campaign, so no HTML! 4 | 5 | You can use the Liquid templating language (https://shopify.github.io/liquid/) here. 6 | 7 | Please make sure to include the following in every campaign: 8 | {{ unsubscribe_link }} 9 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/clipboard-check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/office-building.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/save-as.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/sun.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/calculator.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/collection.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/fast-forward.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/filter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/globe-alt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/music-note.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/rewind.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/server.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /lib/keila_web/views/public_form_layout_view.ex: -------------------------------------------------------------------------------- 1 | defmodule KeilaWeb.PublicFormLayoutView do 2 | use KeilaWeb, :view 3 | use Phoenix.HTML 4 | 5 | def build_styles(styles) do 6 | styles 7 | |> Enum.filter(fn {_, value} -> not is_nil(value) end) 8 | |> Enum.map(fn {key, value} -> ~s{#{key}:#{value}!important} end) 9 | |> Enum.join(";") 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/chat.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/map.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/photograph.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/shopping-cart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/js/hooks/remember-unsaved.js: -------------------------------------------------------------------------------- 1 | let unsaved = false 2 | export const RememberUnsaved = { 3 | mounted() { 4 | window.setUnsavedReminder = (enable) => { 5 | unsaved = enable 6 | } 7 | window.addEventListener("beforeunload", (e) => { 8 | if (unsaved) { 9 | e.preventDefault() 10 | e.returnValue = "" 11 | } 12 | }) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/keila_web/views/shared_sender_admin_view.ex: -------------------------------------------------------------------------------- 1 | defmodule KeilaWeb.SharedSenderAdminView do 2 | use KeilaWeb, :view 3 | alias KeilaWeb.Endpoint 4 | 5 | defp form_path(%{data: %{id: nil}}), 6 | do: Routes.shared_sender_admin_path(Endpoint, :create) 7 | 8 | defp form_path(%{data: %{id: id}}), 9 | do: Routes.shared_sender_admin_path(Endpoint, :update, id) 10 | end 11 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/chat-alt-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/clipboard-copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/cube-transparent.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/document-search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/support.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/repo/migrations/20210104175000_root_group.exs: -------------------------------------------------------------------------------- 1 | defmodule Keila.Repo.Migrations.RootGroup do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create index("groups", ["(parent_id is NULL)"], 6 | using: "btree", 7 | where: "(parent_id IS NULL)", 8 | name: :root_group, 9 | unique: true 10 | ) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/clipboard-list.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/currency-euro.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/hand.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/question-mark-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/receipt-tax.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/scissors.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/document-duplicate.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/printer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /lib/keila/auth/schemas/role_permission.ex: -------------------------------------------------------------------------------- 1 | defmodule Keila.Auth.RolePermission do 2 | use Keila.Schema, prefix: "arp" 3 | 4 | schema "role_permissions" do 5 | belongs_to(:role, Keila.Auth.Role, type: Keila.Auth.Role.Id) 6 | belongs_to(:permission, Keila.Auth.Permission, type: Keila.Auth.Permission.Id) 7 | field(:is_inherited, :boolean) 8 | 9 | timestamps() 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /priv/repo/migrations/20250407180947_add_account_data.exs: -------------------------------------------------------------------------------- 1 | defmodule Keila.Repo.Migrations.AddAccountData do 2 | use Ecto.Migration 3 | require Keila 4 | 5 | Keila.if_cloud do 6 | Code.ensure_loaded!(KeilaCloud.Migrations.AddAccountData) 7 | defdelegate change(), to: KeilaCloud.Migrations.AddAccountData 8 | else 9 | def change do 10 | :ok 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/briefcase.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/chart-pie.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/currency-dollar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/stop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /lib/keila/mailings/deliver_scheduled_campaigns_worker.ex: -------------------------------------------------------------------------------- 1 | defmodule Keila.Mailings.DeliverScheduledCampaignsWorker do 2 | use Oban.Worker, queue: :mailer_scheduler 3 | alias Keila.Mailings 4 | 5 | def perform(%Oban.Job{}) do 6 | Mailings.get_campaigns_to_be_delivered(DateTime.utc_now()) 7 | |> Enum.each(fn c -> 8 | Mailings.deliver_campaign_async(c.id) 9 | end) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/bell.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/volume-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/repo/migrations/20231203235509_add_form_double_opt_in_settings.exs: -------------------------------------------------------------------------------- 1 | defmodule Keila.Repo.Migrations.AddFormDoubleOptInSettings do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table("contacts_forms") do 6 | add :sender_id, references("mailings_senders", on_delete: :nilify_all) 7 | add :template_id, references("templates", on_delete: :nilify_all) 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/inbox.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /extra/keila_cloud_web/views/cloud_admin_view.ex: -------------------------------------------------------------------------------- 1 | require Keila 2 | 3 | Keila.if_cloud do 4 | defmodule KeilaWeb.CloudAdminView do 5 | use Phoenix.View, 6 | root: "extra/keila_cloud_web/templates", 7 | namespace: KeilaWeb 8 | 9 | use Phoenix.HTML 10 | import Phoenix.LiveView.Helpers 11 | 12 | import KeilaWeb.Gettext 13 | alias KeilaWeb.Router.Helpers, as: Routes 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/keila/auth/schemas/permission.ex: -------------------------------------------------------------------------------- 1 | defmodule Keila.Auth.Permission do 2 | use Keila.Schema, prefix: "ap" 3 | 4 | schema "permissions" do 5 | field(:name, :string) 6 | 7 | timestamps() 8 | end 9 | 10 | @spec changeset(t(), Ecto.Changeset.data()) :: Ecto.Changeset.t(t()) 11 | def changeset(struct \\ %__MODULE__{}, params) do 12 | struct 13 | |> cast(params, [:name]) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/chart-bar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/color-swatch.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/globe.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/shield-check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/template.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/social-icons/x-twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/keila/billing/schemas/plan.ex: -------------------------------------------------------------------------------- 1 | defmodule Keila.Billing.Plan do 2 | defstruct [ 3 | :paddle_id, 4 | :name, 5 | :monthly_credits, 6 | :is_active 7 | ] 8 | 9 | def new(paddle_id, name, monthly_credits, active?) do 10 | %__MODULE__{ 11 | paddle_id: paddle_id, 12 | name: name, 13 | monthly_credits: monthly_credits, 14 | is_active: active? 15 | } 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/speakerphone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/view-boards.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/email_templates/hybrid/_block--list_items.liquid: -------------------------------------------------------------------------------- 1 | {% if style == "unordered" %}{% else %}{% endif %} 11 | -------------------------------------------------------------------------------- /priv/repo/migrations/20241117001430_create_instance.exs: -------------------------------------------------------------------------------- 1 | defmodule Keila.Repo.Migrations.CreateInstance do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table("instance") do 6 | add :available_updates, :jsonb, default: "[]" 7 | end 8 | 9 | execute "CREATE UNIQUE INDEX instance_singleton ON instance ((true));", "" 10 | execute "INSERT INTO instance DEFAULT VALUES;", "" 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/exclamation-triangle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/inbox-in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/location-marker.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/shield-exclamation.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/social-icons/twitch.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/light-bulb.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/mail-open.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/phone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /extra/keila_cloud_web/views/cloud_sender_view.ex: -------------------------------------------------------------------------------- 1 | require Keila 2 | 3 | Keila.if_cloud do 4 | defmodule KeilaWeb.CloudSenderView do 5 | use Phoenix.View, 6 | root: "extra/keila_cloud_web/templates", 7 | namespace: KeilaWeb 8 | 9 | use Phoenix.HTML 10 | import Phoenix.LiveView.Helpers 11 | import KeilaWeb.Gettext 12 | import KeilaWeb.ErrorHelpers 13 | import KeilaWeb.IconHelper 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/keila_web/api/validators/api_contact_filter_validator.ex: -------------------------------------------------------------------------------- 1 | defmodule KeilaWeb.Api.ContactFilterValidator do 2 | @moduledoc """ 3 | OpenApiSpex cast module for casting JSON string to Map. 4 | """ 5 | 6 | def cast(ctx = %{value: value}) do 7 | with {:ok, json} <- Jason.decode(value) do 8 | {:ok, json} 9 | else 10 | _ -> OpenApiSpex.Cast.error(ctx, {:invalid_type, :object}) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/identification.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/view-grid-add.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | darkMode: false, // or 'media' or 'class' 3 | content: ["./js/**/*.js", "./css/**/*.*css", "../lib/*_web/**/*.*ex", "../extra/**/*_web/**/*.*ex"], 4 | theme: { 5 | extend: { 6 | colors: { 7 | gray: { 8 | 950: "#0F131A" 9 | } 10 | } 11 | } 12 | }, 13 | plugins: [require("@tailwindcss/forms"), require("@tailwindcss/typography")] 14 | } 15 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/thumb-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/repo/migrations/20210121100329_add_oban_jobs_table.exs: -------------------------------------------------------------------------------- 1 | defmodule Keila.Repo.Migrations.AddObanJobsTable do 2 | use Ecto.Migration 3 | 4 | def up do 5 | Oban.Migrations.up() 6 | end 7 | 8 | # We specify `version: 1` in `down`, ensuring that we'll roll all the way back down if 9 | # necessary, regardless of which version we've migrated `up` to. 10 | def down do 11 | Oban.Migrations.down(version: 1) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/share.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/eye.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/thumb-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/variable.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/social-icons/tiktok.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/keila_web/templates/auth/404.html.heex: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | <%= dgettext("auth", "That didn’t work :-(") %> 5 |

6 |

7 | <%= dgettext("auth", "It seems like your link has expired or is invalid.") %> 8 |

9 |
10 |
11 | -------------------------------------------------------------------------------- /priv/repo/migrations/20240809143953_add_recipient_queued_at.exs: -------------------------------------------------------------------------------- 1 | defmodule Keila.Repo.Migrations.AddRecipientQueuedAt do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table("mailings_recipients") do 6 | add :queued_at, :utc_datetime 7 | end 8 | 9 | execute(&execute_up/0, fn -> :ok end) 10 | end 11 | 12 | defp execute_up() do 13 | repo().query!("UPDATE mailings_recipients SET queued_at=inserted_at") 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/puzzle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/book-open.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/phone-incoming.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/phone-outgoing.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /test/keila_web/helpers/hcaptcha_test.exs: -------------------------------------------------------------------------------- 1 | defmodule KeilaWeb.CaptchaTest do 2 | use ExUnit.Case 3 | import KeilaWeb.Captcha 4 | 5 | @valid_hcaptcha "10000000-aaaa-bbbb-cccc-000000000001" 6 | @invalid_hcaptcha "00000000-aaaa-bbbb-cccc-000000000001" 7 | 8 | test "validate captchas" do 9 | assert true == captcha_valid?(@valid_hcaptcha) 10 | assert false == captcha_valid?(@invalid_hcaptcha) 11 | assert false == captcha_valid?("") 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/keila_web/templates/auth/register_success.html.heex: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | <%= dgettext("auth", "Check your inbox!") %> 5 |

6 |

7 | <%= dgettext("auth", "We’ve sent you a verification link to %{email}.", email: @user.email) %> 8 |

9 |
10 |
11 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/beaker.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/phone-missed-call.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/view-grid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /lib/keila/mailings/sender_adapters/local.ex: -------------------------------------------------------------------------------- 1 | defmodule Keila.Mailings.SenderAdapters.Local do 2 | use Keila.Mailings.SenderAdapters.Adapter 3 | 4 | @impl true 5 | def name, do: "local" 6 | 7 | @impl true 8 | def schema_fields do 9 | [] 10 | end 11 | 12 | @impl true 13 | def changeset(changeset, _params) do 14 | changeset 15 | end 16 | 17 | @impl true 18 | def to_swoosh_config(_struct) do 19 | [adapter: Swoosh.Adapters.Local] 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /priv/repo/migrations/20210816142200_default_contact_status.exs: -------------------------------------------------------------------------------- 1 | defmodule Keila.Repo.Migrations.DefaultContactStatus do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table("contacts") do 6 | modify :status, :smallint, default: 0, null: false 7 | end 8 | 9 | execute(&execute_up/0, fn -> :ok end) 10 | end 11 | 12 | defp execute_up() do 13 | repo().query!( 14 | "UPDATE contacts SET status=$1", 15 | [0] 16 | ) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/qrcode.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/status-offline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/user-group.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/volume-off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /lib/keila_web/helpers/instance_info_plug.ex: -------------------------------------------------------------------------------- 1 | defmodule KeilaWeb.InstanceInfoPlug do 2 | @behaviour Plug 3 | import Plug.Conn 4 | 5 | @impl true 6 | def call(conn, _opts) do 7 | if conn.assigns[:is_admin?] do 8 | updates_available? = Keila.Instance.updates_available?() 9 | 10 | conn 11 | |> assign(:instance_updates_available?, updates_available?) 12 | else 13 | conn 14 | end 15 | end 16 | 17 | @impl true 18 | def init(opts), do: opts 19 | end 20 | -------------------------------------------------------------------------------- /priv/repo/migrations/20210428165000_shared_senders.exs: -------------------------------------------------------------------------------- 1 | defmodule Keila.Repo.Migrations.SharedSenders do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table("mailings_shared_senders") do 6 | add :name, :string, null: false 7 | add :config, :jsonb 8 | 9 | timestamps() 10 | end 11 | 12 | alter table("mailings_senders") do 13 | add :shared_sender_id, references("mailings_shared_senders", on_delete: :delete_all) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/camera.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/finger-print.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /test/keila_web/views/error_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule KeilaWeb.ErrorViewTest do 2 | use KeilaWeb.ConnCase, async: true 3 | 4 | # Bring render/3 and render_to_string/3 for testing custom views 5 | import Phoenix.View 6 | 7 | test "renders 404.html" do 8 | assert render_to_string(KeilaWeb.ErrorView, "404.html", []) == "Not Found" 9 | end 10 | 11 | test "renders 500.html" do 12 | assert render_to_string(KeilaWeb.ErrorView, "500.html", []) == "Internal Server Error" 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /priv/repo/migrations/20210222154306_add_deliver_at_to_campaigns.exs: -------------------------------------------------------------------------------- 1 | defmodule Keila.Repo.Migrations.AddDeliverAtToCampaigns do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table("mailings_campaigns") do 6 | add :scheduled_for, :utc_datetime 7 | end 8 | 9 | create index("mailings_campaigns", [:scheduled_for, :sent_at], 10 | where: "scheduled_for is not null and sent_at is null", 11 | name: :scheduled_campaigns 12 | ) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/cake.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/fire.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/eye-off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/repo/migrations/20220114154000_add_campaign_custom_data.exs: -------------------------------------------------------------------------------- 1 | defmodule Keila.Repo.Migrations.AddCampaignCustomData do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table("mailings_campaigns") do 6 | add :data, :jsonb 7 | end 8 | 9 | create constraint("mailings_campaigns", :max_data_size, 10 | check: "pg_column_size(data) <= 32000" 11 | ) 12 | 13 | create constraint("contacts", :max_data_size, check: "pg_column_size(data) <= 8000") 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /priv/vendor/social-icons/discourse.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/keila_web/templates/auth/activate_resend_success.html.heex: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | <%= dgettext("auth", "Check your inbox!") %> 5 |

6 |

7 | <%= dgettext("auth", "We’ve resent your verification link to %{email}.", 8 | email: @current_user.email 9 | ) %> 10 |

11 |
12 |
13 | -------------------------------------------------------------------------------- /priv/repo/migrations/20230120223000_add_segment_default_filter.exs: -------------------------------------------------------------------------------- 1 | defmodule Keila.Repo.Migrations.AddSegmentDefaultFilter do 2 | use Ecto.Migration 3 | 4 | def change do 5 | execute(&execute_up/0, &execute_down/0) 6 | end 7 | 8 | defp execute_up() do 9 | repo().query!("UPDATE contacts_segments SET filter='{}' WHERE filter IS NULL") 10 | 11 | alter table("contacts_segments") do 12 | modify :filter, :jsonb, default: %{} 13 | end 14 | end 15 | 16 | defp execute_down(), do: :ok 17 | end 18 | -------------------------------------------------------------------------------- /priv/vendor/social-icons/facebook.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/keila/mailings/schemas/campaign_settings.ex: -------------------------------------------------------------------------------- 1 | defmodule Keila.Mailings.Campaign.Settings do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | embedded_schema do 6 | field(:type, Ecto.Enum, values: [:text, :markdown, :block, :mjml]) 7 | field(:enable_wysiwyg, :boolean, default: true) 8 | field(:do_not_track, :boolean, default: false) 9 | end 10 | 11 | def changeset(struct \\ %__MODULE__{}, params) do 12 | struct 13 | |> cast(params, [:type, :enable_wysiwyg, :do_not_track]) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/keila_web/helpers/meta/meta_plug.ex: -------------------------------------------------------------------------------- 1 | defmodule KeilaWeb.Meta.Plug do 2 | @moduledoc """ 3 | Simple Plug for holding page meta information (title, 4 | description, etc). 5 | 6 | Use with helper functions from `KeilaWeb.Meta`. 7 | """ 8 | import Plug.Conn 9 | 10 | @spec init(term()) :: term() 11 | def init(_opts) do 12 | nil 13 | end 14 | 15 | @spec call(Plug.Conn.t(), term) :: Plug.Conn.t() 16 | def call(conn, _opts) do 17 | conn 18 | |> put_private(:keila_meta, %{}) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/keila/accounts/schemas/account.ex: -------------------------------------------------------------------------------- 1 | defmodule Keila.Accounts.Account do 2 | use Keila.Schema, prefix: "acc" 3 | 4 | schema "accounts" do 5 | belongs_to(:parent, __MODULE__, type: __MODULE__.Id) 6 | belongs_to(:group, Keila.Auth.Group, type: Keila.Auth.Group.Id) 7 | 8 | Keila.if_cloud do 9 | use KeilaCloud.Accounts.Account 10 | end 11 | 12 | timestamps() 13 | end 14 | 15 | def changeset(struct \\ %__MODULE__{}, params) do 16 | struct 17 | |> cast(params, [:group_id]) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /priv/repo/migrations/20211116040700_segmentations.exs: -------------------------------------------------------------------------------- 1 | defmodule Keila.Repo.Migrations.Segments do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table("contacts_segments") do 6 | add :project_id, references("projects", on_delete: :delete_all) 7 | add :name, :string 8 | add :filter, :jsonb 9 | 10 | timestamps() 11 | end 12 | 13 | alter table("mailings_campaigns") do 14 | add :segment_id, references("contacts_segments", on_delete: :nilify_all) 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /priv/vendor/keila-icons/list-unordered.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /priv/repo/migrations/20210630205000_subscription_upserts.exs: -------------------------------------------------------------------------------- 1 | defmodule Keila.Repo.Migrations.SubscriptionUpserts do 2 | use Ecto.Migration 3 | 4 | def up do 5 | alter table("billing_subscriptions") do 6 | modify :update_url, :string, null: true 7 | modify :cancel_url, :string, null: true 8 | end 9 | end 10 | 11 | def down do 12 | alter table("billing_subscriptions") do 13 | modify :update_url, :string, null: false 14 | modify :cancel_url, :string, null: false 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /.zed/settings.json: -------------------------------------------------------------------------------- 1 | // Folder-specific settings 2 | // 3 | // For a full list of overridable settings, and general information on folder-specific settings, 4 | // see the documentation: https://zed.dev/docs/configuring-zed#settings-files 5 | { 6 | "languages": { 7 | "JavaScript": { 8 | "formatter": { 9 | "external": { 10 | "command": "dprint", 11 | "arguments": ["fmt", "--stdin", "{buffer_path}"] 12 | } 13 | } 14 | }, 15 | "SCSS": { 16 | "tab_size": 2 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/keila_web/templates/sender/verification_failure.html.heex: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | <%= gettext("Sender verification not successful.") %> 5 |

6 |

7 | <%= gettext( 8 | "There was an error when trying to verify your sender. Please try again or contact an administrator." 9 | ) %> 10 |

11 |
12 |
13 | -------------------------------------------------------------------------------- /lib/keila/accounts/schemas/credit.ex: -------------------------------------------------------------------------------- 1 | defmodule Keila.Accounts.CreditTransaction do 2 | use Keila.Schema, prefix: "acc" 3 | 4 | schema "accounts_credit_transactions" do 5 | field :amount, :integer 6 | field :expires_at, :utc_datetime 7 | 8 | belongs_to(:account, Keila.Accounts.Account, type: Keila.Accounts.Account.Id) 9 | 10 | timestamps(updated_at: false) 11 | end 12 | 13 | def changeset(struct \\ %__MODULE__{}, params) do 14 | struct 15 | |> cast(params, [:amount, :expires_at, :account_id]) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/keila_web/templates/auth/reset_success.html.heex: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | <%= dgettext("auth", "Check your inbox!") %> 5 |

6 |

7 | <%= dgettext( 8 | "auth", 9 | "If you have an account, we’ve sent you a verification link to %{email}.", 10 | email: @email 11 | ) %> 12 |

13 |
14 |
15 | -------------------------------------------------------------------------------- /lib/keila/tracking/schemas/link.ex: -------------------------------------------------------------------------------- 1 | defmodule Keila.Tracking.Link do 2 | use Keila.Schema, prefix: "tl" 3 | 4 | schema "tracking_links" do 5 | field :url, :string 6 | belongs_to(:campaign, Keila.Mailings.Campaign, type: Keila.Mailings.Campaign.Id) 7 | has_many(:clicks, Keila.Tracking.Click) 8 | 9 | timestamps(updated_at: false) 10 | end 11 | 12 | def changeset(struct \\ %__MODULE__{}, params) do 13 | struct 14 | |> cast(params, [:url, :campaign_id]) 15 | |> validate_required([:url, :campaign_id]) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /priv/repo/migrations/20250220063317_add_accounts_credit_transactions_on_delete.exs: -------------------------------------------------------------------------------- 1 | defmodule Keila.Repo.Migrations.AddAccountsCreditTransactionsOnDelete do 2 | use Ecto.Migration 3 | 4 | def up do 5 | drop constraint( 6 | "accounts_credit_transactions", 7 | "accounts_credit_transactions_account_id_fkey" 8 | ) 9 | 10 | alter table("accounts_credit_transactions") do 11 | modify :account_id, references("accounts", on_delete: :delete_all) 12 | end 13 | end 14 | 15 | def down do 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/keila/tracking/schemas/click.ex: -------------------------------------------------------------------------------- 1 | defmodule Keila.Tracking.Click do 2 | use Keila.Schema 3 | 4 | schema "tracking_clicks" do 5 | belongs_to(:link, Keila.Tracking.Link, type: Keila.Tracking.Link.Id) 6 | belongs_to(:recipient, Keila.Mailings.Recipient, type: Keila.Mailings.Recipient.Id) 7 | 8 | timestamps(updated_at: false) 9 | end 10 | 11 | def changeset(struct \\ %__MODULE__{}, params) do 12 | struct 13 | |> cast(params, [:link_id, :recipient_id]) 14 | |> validate_required([:link_id, :recipient_id]) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/truck.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /lib/keila/mailings/sender_adapters/shared_local.ex: -------------------------------------------------------------------------------- 1 | defmodule Keila.Mailings.SenderAdapters.Shared.Local do 2 | use Keila.Mailings.SenderAdapters.Adapter 3 | 4 | @impl true 5 | def name, do: "shared_local" 6 | 7 | @impl true 8 | def schema_fields do 9 | [] 10 | end 11 | 12 | @impl true 13 | def changeset(changeset, _params) do 14 | changeset 15 | end 16 | 17 | @impl true 18 | def to_swoosh_config(%{shared_sender: shared_sender}) do 19 | Keila.Mailings.SenderAdapters.Local.to_swoosh_config(shared_sender) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/repo/migrations/20210311225200_templates.exs: -------------------------------------------------------------------------------- 1 | defmodule Keila.Repo.Migrations.Templates do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table("templates") do 6 | add :project_id, references("projects", on_delete: :delete_all) 7 | 8 | add :name, :text 9 | add :body, :text 10 | add :styles, :text 11 | add :assigns, :jsonb 12 | 13 | timestamps() 14 | end 15 | 16 | alter table("mailings_campaigns") do 17 | add :template_id, references("templates", on_delete: :nilify_all) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /priv/repo/migrations/20230604141200_add_contacts_form_params_table.exs: -------------------------------------------------------------------------------- 1 | defmodule Keila.Repo.Migrations.AddContactsFormParams do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table("contacts_form_params") do 6 | add :params, :jsonb 7 | 8 | add :form_id, references("contacts_forms", on_delete: :delete_all) 9 | add :expires_at, :utc_datetime 10 | timestamps() 11 | end 12 | 13 | create constraint("contacts_form_params", :max_data_size, 14 | check: "pg_column_size(params) <= 8000" 15 | ) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /dprint.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript": { 3 | "semiColons": "asi", 4 | "trailingCommas": "never" 5 | }, 6 | "json": {}, 7 | "includes": ["**/*.{ts,tsx,js,jsx,cjs,mjs,json}"], 8 | "excludes": [ 9 | "**/node_modules", 10 | "**/*-lock.json", 11 | "**/dist", 12 | "deps", 13 | "assets/static/vendor", 14 | "priv/vendor", 15 | "priv/static", 16 | "**/priv/cldr", 17 | "**/.elixir_ls" 18 | ], 19 | "plugins": [ 20 | "https://plugins.dprint.dev/typescript-0.80.2.wasm", 21 | "https://plugins.dprint.dev/json-0.17.0.wasm" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /lib/keila_web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule KeilaWeb.ErrorView do 2 | use KeilaWeb, :view 3 | 4 | # If you want to customize a particular status code 5 | # for a certain format, you may uncomment below. 6 | # def render("500.html", _assigns) do 7 | # "Internal Server Error" 8 | # end 9 | 10 | # By default, Phoenix returns the status message from 11 | # the template name. For example, "404.html" becomes 12 | # "Not Found". 13 | def template_not_found(template, _assigns) do 14 | Phoenix.Controller.status_message_from_template(template) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /priv/vendor/keila-icons/block.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /extra/keila_cloud_web/controllers/cloud_account_controller.ex: -------------------------------------------------------------------------------- 1 | require Keila 2 | 3 | Keila.if_cloud do 4 | defmodule KeilaWeb.CloudAccountController do 5 | use KeilaWeb, :controller 6 | import Phoenix.LiveView.Controller 7 | 8 | def onboarding(conn, _) do 9 | live_render(conn, KeilaWeb.CloudAccountOnboardingLive, 10 | session: %{ 11 | "current_account" => conn.assigns.current_account, 12 | "current_user" => conn.assigns.current_user, 13 | "locale" => Gettext.get_locale() 14 | } 15 | ) 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /priv/repo/migrations/20221227145500_add_recipient_states.exs: -------------------------------------------------------------------------------- 1 | defmodule Keila.Repo.Migrations.AddRecipientStates do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table("mailings_recipients") do 6 | add :soft_bounce_received_at, :utc_datetime 7 | add :hard_bounce_received_at, :utc_datetime 8 | add :complaint_received_at, :utc_datetime 9 | add :unsubscribed_at, :utc_datetime 10 | end 11 | 12 | alter table("contacts_events") do 13 | add :recipient_id, references("mailings_recipients", on_delete: :nilify_all) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /priv/vendor/font-awesome/spinner.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /priv/vendor/social-icons/youtube.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /priv/repo/migrations/20220523235000_add_files.exs: -------------------------------------------------------------------------------- 1 | defmodule Keila.Repo.Migrations.AddFiles do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table("files", primary_key: false) do 6 | add :uuid, :binary, length: 16, primary_key: true 7 | add :filename, :string 8 | add :type, :string 9 | add :size, :integer 10 | add :sha256, :binary, length: 32 11 | add :adapter, :string, length: 16 12 | add :adapter_data, :jsonb 13 | 14 | add :project_id, references("projects", on_delete: :nilify_all) 15 | 16 | timestamps() 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /assets/js/hooks/file_manager.js: -------------------------------------------------------------------------------- 1 | const FileManager = { 2 | mounted() { 3 | this.handleEvent("remove_file", ({ id, src }) => { 4 | this.el.dispatchEvent( 5 | new CustomEvent("x-file-removed", { 6 | detail: { id, src }, 7 | bubbles: true 8 | }) 9 | ) 10 | }) 11 | 12 | this.handleEvent("file_in_use", ({ campaigns }) => { 13 | this.el.dispatchEvent( 14 | new CustomEvent("x-file-in-use", { 15 | detail: { campaigns }, 16 | bubbles: true 17 | }) 18 | ) 19 | }) 20 | } 21 | } 22 | 23 | export { FileManager } 24 | -------------------------------------------------------------------------------- /lib/keila_web/views/campaign_view.ex: -------------------------------------------------------------------------------- 1 | defmodule KeilaWeb.CampaignView do 2 | use KeilaWeb, :view 3 | require Keila 4 | import Ecto.Changeset, only: [get_field: 2] 5 | 6 | def plain_text_preview(text) do 7 | """ 8 | 9 | 10 | 11 | 12 |
13 | #{text} 14 |
15 | 16 | 17 | """ 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /priv/repo/migrations/20220905125000_emails_to_citext.exs: -------------------------------------------------------------------------------- 1 | defmodule Keila.Repo.Migrations.EmailsToCitext do 2 | use Ecto.Migration 3 | 4 | def up do 5 | execute "CREATE EXTENSION IF NOT EXISTS citext" 6 | 7 | alter table("users") do 8 | modify :email, :citext, null: false 9 | end 10 | 11 | alter table("contacts") do 12 | modify :email, :citext 13 | end 14 | end 15 | 16 | def down do 17 | alter table("users") do 18 | modify :email, :string, null: false 19 | end 20 | 21 | alter table("contacts") do 22 | modify :email, :string 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/academic-cap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /priv/email_templates/hybrid/_block--button.liquid: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 |
7 | {{block.data.label }} 8 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /priv/vendor/social-icons/xing.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /priv/vendor/font-awesome/align-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /priv/vendor/hero-icons-outline/badge-check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /lib/keila_web/templates/auth/registration_disabled.html.heex: -------------------------------------------------------------------------------- 1 |
2 |
6 |

7 | <%= gettext("Registration disabled.") %> 8 |

9 |

10 | <%= gettext("Please contact an administrator.") %> 11 |

12 |

13 | <%= gettext("Already have an account?") %> 14 | <%= gettext("Sign in.") %> 15 |

16 |
17 |
18 | -------------------------------------------------------------------------------- /priv/vendor/font-awesome/align-justify.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /priv/vendor/font-awesome/align-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /priv/vendor/keila-icons/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /lib/keila/instance/update_cron_worker.ex: -------------------------------------------------------------------------------- 1 | defmodule Keila.Instance.UpdateCronWorker do 2 | use Oban.Worker, 3 | queue: :updater, 4 | unique: [ 5 | period: :infinity, 6 | states: [:available, :scheduled, :executing] 7 | ] 8 | 9 | use Keila.Repo 10 | alias Keila.Instance 11 | 12 | @impl true 13 | def perform(_job) do 14 | if Instance.update_checks_enabled?() do 15 | releases = Instance.fetch_updates() 16 | 17 | Instance.get_instance() 18 | |> change() 19 | |> put_embed(:available_updates, releases) 20 | |> Repo.update() 21 | else 22 | :ok 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /priv/vendor/font-awesome/align-center.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/keila_web/api/views/api_sender_view.ex: -------------------------------------------------------------------------------- 1 | defmodule KeilaWeb.ApiSenderView do 2 | use KeilaWeb, :view 3 | alias Keila.Pagination 4 | 5 | def render("senders.json", %{senders: senders = %Pagination{}}) do 6 | %{ 7 | "meta" => %{ 8 | "page" => senders.page, 9 | "page_count" => senders.page_count, 10 | "count" => senders.count 11 | }, 12 | "data" => Enum.map(senders.data, &sender_data/1) 13 | } 14 | end 15 | 16 | @properties [:id, :name, :from_email, :from_name, :reply_to_email, :reply_to_name] 17 | defp sender_data(sender) do 18 | sender 19 | |> Map.take(@properties) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /extra/keila_cloud_web/templates/cloud_account/_completed.html.heex: -------------------------------------------------------------------------------- 1 |

Thank you!

2 | <%= dgettext_md("cloud", """ 3 | We will now perform a manual review of your data. This usually doesn't take longer than one working day. 4 | 5 | In the meantime, you can continue to use Keila. Sending emails will be enabled for your account 6 | once the review has been completed. 7 | """) %> 8 | 9 |
10 | 15 | <%= dgettext("cloud", "Back to project list") %> 16 | 17 |
18 | -------------------------------------------------------------------------------- /priv/repo/migrations/20210319145300_form_timestamps.exs: -------------------------------------------------------------------------------- 1 | defmodule Keila.Repo.Migrations.FormTimestamps do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table("contacts_forms") do 6 | timestamps(null: true) 7 | end 8 | 9 | execute(&execute_up/0, &execute_down/0) 10 | end 11 | 12 | defp execute_up() do 13 | repo().query!("UPDATE contacts_forms SET updated_at=NOW(), inserted_at=NOW()") 14 | 15 | alter table("contacts_forms") do 16 | modify :inserted_at, :naive_datetime, null: false 17 | modify :updated_at, :naive_datetime, null: false 18 | end 19 | end 20 | 21 | defp execute_down(), do: :ok 22 | end 23 | -------------------------------------------------------------------------------- /lib/keila/auth/schemas/role.ex: -------------------------------------------------------------------------------- 1 | defmodule Keila.Auth.Role do 2 | use Keila.Schema, prefix: "ar" 3 | 4 | schema "roles" do 5 | field(:name, :string) 6 | has_many(:role_permissions, Keila.Auth.RolePermission) 7 | 8 | belongs_to(:parent, __MODULE__, 9 | foreign_key: :parent_id, 10 | references: :id, 11 | type: Keila.Auth.Role.Id 12 | ) 13 | 14 | timestamps() 15 | end 16 | 17 | @spec changeset(t(), Ecto.Changeset.data()) :: Ecto.Changeset.t(t()) 18 | def changeset(struct \\ %__MODULE__{}, params) do 19 | struct 20 | |> cast(params, [:name, :parent_id]) 21 | |> foreign_key_constraint(:parent_id) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /priv/vendor/social-icons/bluesky.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /priv/vendor/social-icons/linkedin.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/keila_web/templates/sender/verification_success.html.heex: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | <%= gettext("Sender verified!") %> 5 |

6 |

7 | <%= gettext( 8 | "Thank you for verifying your sender. You can now start sending emails with Keila." 9 | ) %> 10 |

11 |

12 | 13 | <%= gettext("Back to your project") %> 14 | 15 |

16 |
17 |
18 | -------------------------------------------------------------------------------- /assets/css/_header.scss: -------------------------------------------------------------------------------- 1 | 2 | #main-menu { 3 | #main-menu-toggle:checked { 4 | &~* #show-main-menu { 5 | display: none; 6 | } 7 | &~ nav { 8 | @apply flex; 9 | } 10 | } 11 | 12 | #main-menu-toggle:not(:checked) ~ * #hide-main-menu { 13 | display: none; 14 | } 15 | } 16 | 17 | .menu-link { 18 | @apply flex gap-2 items-center text-white hover:bg-emerald-800 hover:text-white px-3 py-2 rounded-md text-sm font-medium; 19 | 20 | &.menu-link--active { 21 | @apply bg-emerald-800 text-white; 22 | } 23 | 24 | &.sml-4.menu-link--active { 25 | @apply bg-emerald-900 text-white; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /extra/keila_cloud/schemas/account.ex: -------------------------------------------------------------------------------- 1 | require Keila 2 | 3 | Keila.if_cloud do 4 | defmodule KeilaCloud.Accounts.Account do 5 | defmacro __using__(_opts) do 6 | quote do 7 | embeds_one(:contact_data, KeilaCloud.Accounts.Account.ContactData) 8 | 9 | embeds_one( 10 | :onboarding_review_data, 11 | KeilaCloud.Accounts.Account.OnboardingReviewData 12 | ) 13 | 14 | field :status, Ecto.Enum, 15 | values: [ 16 | default: 0, 17 | active: 1, 18 | suspended: -1, 19 | onboarding_required: 10, 20 | under_review: 11 21 | ] 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /assets/js/campaign-editors/block/blocks/separator.js: -------------------------------------------------------------------------------- 1 | export default class Separator { 2 | constructor(_data) { 3 | this.data = {} 4 | } 5 | 6 | render() { 7 | const el = document.createElement("div") 8 | el.appendChild(document.createElement("hr")) 9 | 10 | return el 11 | } 12 | 13 | save(_blockContent) { 14 | return {} 15 | } 16 | 17 | static get contentless() { 18 | return true 19 | } 20 | 21 | static get toolbox() { 22 | return { 23 | title: document.querySelector("#block-container-assets .editor-separator-title").innerText, 24 | icon: document.querySelector("#block-container-assets .icon-horizontal-rule").innerHTML 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/keila/mailings/sender_adapters/sendgrid.ex: -------------------------------------------------------------------------------- 1 | defmodule Keila.Mailings.SenderAdapters.Sendgrid do 2 | use Keila.Mailings.SenderAdapters.Adapter 3 | 4 | @impl true 5 | def name, do: "sendgrid" 6 | 7 | @impl true 8 | def schema_fields do 9 | [ 10 | sendgrid_api_key: :string 11 | ] 12 | end 13 | 14 | @impl true 15 | def changeset(changeset, params) do 16 | changeset 17 | |> cast(params, [:sendgrid_api_key]) 18 | |> validate_required([:sendgrid_api_key]) 19 | end 20 | 21 | @impl true 22 | def to_swoosh_config(%{config: config}) do 23 | [ 24 | adapter: Swoosh.Adapters.Sendgrid, 25 | api_key: config.sendgrid_api_key 26 | ] 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/keila_web/templates/auth/activate_success.html.heex: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | <%= dgettext("auth", "Welcome to Keila!") %> 5 |

6 |

7 | <%= dgettext("auth", "Thanks for verifying your email.") %> 8 |

9 |

10 | 11 | <%= render_icon(:paper_airplane) %> 12 | <%= dgettext("auth", "Get started!") %> 13 | 14 |

15 |
16 |
17 | -------------------------------------------------------------------------------- /lib/keila_web/templates/project/_empty_state_campaigns.html.heex: -------------------------------------------------------------------------------- 1 | 4 |
5 |

6 | <%= gettext("Ready, set, send!") %> 7 |

8 | <%= gettext_md(""" 9 | You’re ready to send your first campaign with Keila. 10 | """) %> 11 |

12 | 16 | <%= render_icon(:document_add) %> 17 | <%= gettext("Create your first campaign") %> 18 | 19 |

20 |
21 | -------------------------------------------------------------------------------- /lib/keila_web/templates/public_form/double_opt_in_failure.html.heex: -------------------------------------------------------------------------------- 1 |
2 | <%= render_h1(@form) %> 3 |

4 | <%= if @form.settings.failure_text && @form.settings.failure_text != "" do %> 5 | <%= @form.settings.failure_text %> 6 | <% else %> 7 | <%= gettext("This confirmation link has expired or has already been used.") %> 8 | <% end %> 9 |

10 |

11 | <%= gettext("Please submit the form again to receive a new confirmation link.") %> 12 |

13 |

14 | 15 | <%= gettext("Back to form") %> 16 | 17 |

18 | <%= render_fine_print(@form) %> 19 |
20 | -------------------------------------------------------------------------------- /priv/vendor/keila-icons/mjml.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/keila_web/templates/auth/reset_change_password_success.html.heex: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | <%= gettext("Your password was changed!") %> 5 |

6 |

7 | <%= dgettext("auth", "You can now continue using Keila.") %> 8 |

9 |

10 | 11 | <%= render_icon(:login) %> 12 | <%= dgettext("auth", "Log in with your new password") %> 13 | 14 |

15 |
16 |
17 | -------------------------------------------------------------------------------- /test/keila_web/controllers/auth_controller_login_disabled_test.exs: -------------------------------------------------------------------------------- 1 | defmodule KeilaWeb.AuthControllerDisabledTest do 2 | use KeilaWeb.ConnCase, async: false 3 | 4 | setup do 5 | Application.put_env(:keila, :registration_disabled, true) 6 | 7 | on_exit(fn -> 8 | Application.put_env(:keila, :registration_disabled, false) 9 | end) 10 | end 11 | 12 | @tag :auth_controller 13 | test "Registration can be disabled", %{conn: conn} do 14 | conn = get(conn, Routes.auth_path(conn, :register)) 15 | assert html_response(conn, 200) =~ "Registration disabled" 16 | 17 | conn = post(conn, Routes.auth_path(conn, :post_register)) 18 | assert html_response(conn, 200) =~ "Registration disabled" 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /.github/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /lib/keila_web/api/plugs/api_authorization_plug.ex: -------------------------------------------------------------------------------- 1 | defmodule KeilaWeb.Api.Plugs.Authorization do 2 | @behaviour Plug 3 | import Plug.Conn 4 | alias Keila.Auth 5 | alias Keila.Projects 6 | 7 | def init(_), do: nil 8 | 9 | def call(conn, _) do 10 | with ["Bearer " <> api_key] <- get_req_header(conn, "authorization"), 11 | %Auth.Token{data: %{"project_id" => project_id}, user_id: user_id} <- 12 | Auth.find_api_key(api_key), 13 | project = %Projects.Project{} <- 14 | Projects.get_user_project(user_id, project_id) do 15 | conn 16 | |> assign(:current_project, project) 17 | else 18 | _ -> conn |> KeilaWeb.Api.Errors.send_403() |> halt() 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/keila_web/views/contact_view.ex: -------------------------------------------------------------------------------- 1 | defmodule KeilaWeb.ContactView do 2 | use KeilaWeb, :view 3 | use Phoenix.HTML 4 | 5 | def table_sort_button(assigns) do 6 | active? = assigns[:current_key] == assigns[:key] 7 | 8 | ~H""" 9 | 22 | """ 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/keila_web/helpers/project/project_plug.ex: -------------------------------------------------------------------------------- 1 | defmodule KeilaWeb.ProjectPlug do 2 | @moduledoc """ 3 | This Plug adds a `current_project` assign from the `:project_id` path param. 4 | 5 | Halts the pipeline with 404 if the current user is not authorized to access 6 | the project or if the project doesn‘t exist. 7 | """ 8 | import Plug.Conn 9 | alias Keila.Projects 10 | 11 | def init(_), do: [] 12 | 13 | def call(conn, _) do 14 | project_id = conn.path_params["project_id"] 15 | user = conn.assigns.current_user 16 | 17 | case Projects.get_user_project(user.id, project_id) do 18 | nil -> conn |> put_status(404) |> halt() 19 | project -> assign(conn, :current_project, project) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /priv/repo/migrations/20250707201249_add_verified_from_email_to_senders.exs: -------------------------------------------------------------------------------- 1 | defmodule Keila.Repo.Migrations.AddVerifiedFromEmailToSenders do 2 | use Ecto.Migration 3 | 4 | def up do 5 | alter table("mailings_senders") do 6 | add :verified_from_email, :string 7 | end 8 | 9 | # Data migration to set verified_from_email based on sender type 10 | execute """ 11 | UPDATE mailings_senders 12 | SET verified_from_email = config->>'shared_ses_verification_requested_for' 13 | WHERE config->>'type' = 'shared_ses' AND config->>'shared_ses_verified_at' IS NOT NULL 14 | """ 15 | end 16 | 17 | def down do 18 | alter table("mailings_senders") do 19 | remove :verified_from_email 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/keila_web/api/views/api_segment_view.ex: -------------------------------------------------------------------------------- 1 | defmodule KeilaWeb.ApiSegmentView do 2 | use KeilaWeb, :view 3 | alias Keila.Pagination 4 | 5 | def render("segments.json", %{segments: segments = %Pagination{}}) do 6 | %{ 7 | "meta" => %{ 8 | "page" => segments.page, 9 | "page_count" => segments.page_count, 10 | "count" => segments.count 11 | }, 12 | "data" => Enum.map(segments.data, &segment_data/1) 13 | } 14 | end 15 | 16 | def render("segment.json", %{segment: segment}) do 17 | %{ 18 | "data" => segment_data(segment) 19 | } 20 | end 21 | 22 | @properties [:id, :name, :filter] 23 | defp segment_data(segment) do 24 | segment 25 | |> Map.take(@properties) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/keila_web/helpers/plain_text_json_parser/plain_text_json_parser.ex: -------------------------------------------------------------------------------- 1 | defmodule KeilaWeb.PlainTextJSONParser do 2 | @moduledoc """ 3 | This Parser plug is used to parse requests with content-type `text/plain` as JSON. 4 | 5 | It’s necessary to use this plug when external applications (e.g. webhooks) 6 | use `text/plain` instead of `application/json`. 7 | """ 8 | 9 | @behaviour Plug.Parsers 10 | 11 | @impl true 12 | def init(opts) do 13 | Plug.Parsers.JSON.init(opts) 14 | end 15 | 16 | @impl true 17 | def parse(conn, "text", "plain", params, opts) do 18 | Plug.Parsers.JSON.parse(conn, "application", "json", params, opts) 19 | end 20 | 21 | def parse(conn, _type, _subtype, _params, _opts) do 22 | {:next, conn} 23 | end 24 | end 25 | --------------------------------------------------------------------------------