mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-06-03 05:35:02 -04:00
Feature/email support (#720)
* feat(frontend): ✨ add UI for testing email configuration * feat(backend): ✨ add email service with common templates (WIP) * test(backend): ✅ add basic tests for email configuration * set defaults * add email variables Co-authored-by: Hayden <hay-kot@pm.me>
This commit is contained in:
parent
c0dd07f9e7
commit
b7b8aa9a08
@ -15,6 +15,16 @@ services:
|
|||||||
- ALLOW_SIGNUP=true
|
- ALLOW_SIGNUP=true
|
||||||
- API_URL=http://mealie-api:80
|
- API_URL=http://mealie-api:80
|
||||||
|
|
||||||
|
# =====================================
|
||||||
|
# Email Configuration
|
||||||
|
# - SMTP_HOST=
|
||||||
|
# - SMTP_PORT=587
|
||||||
|
# - SMTP_FROM_NAME=Mealie
|
||||||
|
# - SMTP_TLS=true
|
||||||
|
# - SMTP_FROM_EMAIL=
|
||||||
|
# - SMTP_USER=
|
||||||
|
# - SMTP_PASSWORD=
|
||||||
|
|
||||||
# =====================================
|
# =====================================
|
||||||
# Light Mode Config
|
# Light Mode Config
|
||||||
- THEME_LIGHT_PRIMARY=#E58325
|
- THEME_LIGHT_PRIMARY=#E58325
|
||||||
|
@ -112,48 +112,79 @@ services:
|
|||||||
POSTGRES_USER: mealie
|
POSTGRES_USER: mealie
|
||||||
```
|
```
|
||||||
|
|
||||||
## mealie-api Env Variables
|
## API Environment Variables
|
||||||
|
|
||||||
| Variables | Default | Description |
|
### General
|
||||||
| ----------------- | :-------------------: | --------------------------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| PUID | 911 | UserID permissions between host OS and container |
|
|
||||||
| PGID | 911 | GroupID permissions between host OS and container |
|
|
||||||
| DEFAULT_GROUP | Home | The default group for users |
|
|
||||||
| DEFAULT_EMAIL | changeme@email.com | The default username for the superuser |
|
|
||||||
| BASE_URL | http://localhost:8080 | Used for Notifications |
|
|
||||||
| DB_ENGINE | sqlite | Optional: 'sqlite', 'postgres' |
|
|
||||||
| POSTGRES_USER | mealie | Postgres database user |
|
|
||||||
| POSTGRES_PASSWORD | mealie | Postgres database password |
|
|
||||||
| POSTGRES_SERVER | postgres | Postgres database server address |
|
|
||||||
| POSTGRES_PORT | 5432 | Postgres database port |
|
|
||||||
| POSTGRES_DB | mealie | Postgres database name |
|
|
||||||
| TOKEN_TIME | 2 | The time in hours that a login/auth token is valid |
|
|
||||||
| API_PORT | 9000 | The port exposed by backend API. **Do not change this if you're running in Docker** |
|
|
||||||
| API_DOCS | True | Turns on/off access to the API documentation locally. |
|
|
||||||
| TZ | UTC | Must be set to get correct date/time on the server |
|
|
||||||
| WORKERS_PER_CORE | 1 | Set the number of workers to the number of CPU cores multiplied by this value (Value \* CPUs). More info [here][workers_per_core] |
|
|
||||||
| MAX_WORKERS | | Set the maximum number of workers to use. Default is not set meaning unlimited. More info [here][max_workers] |
|
|
||||||
| WEB_CONCURRENCY | 2 | Override the automatic definition of number of workers. More info [here][web_concurrency] |
|
|
||||||
|
|
||||||
## mealie-frontend Env Variables
|
| Variables | Default | Description |
|
||||||
|
| ------------- | :-------------------: | ----------------------------------------------------------------------------------- |
|
||||||
|
| PUID | 911 | UserID permissions between host OS and container |
|
||||||
|
| PGID | 911 | GroupID permissions between host OS and container |
|
||||||
|
| DEFAULT_GROUP | Home | The default group for users |
|
||||||
|
| DEFAULT_EMAIL | changeme@email.com | The default username for the superuser |
|
||||||
|
| BASE_URL | http://localhost:8080 | Used for Notifications |
|
||||||
|
| TOKEN_TIME | 2 | The time in hours that a login/auth token is valid |
|
||||||
|
| API_PORT | 9000 | The port exposed by backend API. **Do not change this if you're running in Docker** |
|
||||||
|
| API_DOCS | True | Turns on/off access to the API documentation locally. |
|
||||||
|
| TZ | UTC | Must be set to get correct date/time on the server |
|
||||||
|
|
||||||
| Variables | Default | Description |
|
|
||||||
| --------------------- | :-----: | ---------------------------------- |
|
### Database
|
||||||
| ALLOW_SIGNUP | true | Allows anyone to signup for Mealie |
|
|
||||||
| THEME_LIGHT_PRIMARY | #E58325 | Light Theme Config Variable |
|
| Variables | Default | Description |
|
||||||
| THEME_LIGHT_ACCENT | #007A99 | Light Theme Config Variable |
|
| ----------------- | :------: | -------------------------------- |
|
||||||
| THEME_LIGHT_SECONDARY | #973542 | Light Theme Config Variable |
|
| DB_ENGINE | sqlite | Optional: 'sqlite', 'postgres' |
|
||||||
| THEME_LIGHT_SUCCESS | #43A047 | Light Theme Config Variable |
|
| POSTGRES_USER | mealie | Postgres database user |
|
||||||
| THEME_LIGHT_INFO | #1976D2 | Light Theme Config Variable |
|
| POSTGRES_PASSWORD | mealie | Postgres database password |
|
||||||
| THEME_LIGHT_WARNING | #FF6D00 | Light Theme Config Variable |
|
| POSTGRES_SERVER | postgres | Postgres database server address |
|
||||||
| THEME_LIGHT_ERROR | #EF5350 | Light Theme Config Variable |
|
| POSTGRES_PORT | 5432 | Postgres database port |
|
||||||
| DARK_LIGHT_PRIMARY | #E58325 | Dark Theme Config Variable |
|
| POSTGRES_DB | mealie | Postgres database name |
|
||||||
| DARK_LIGHT_ACCENT | #007A99 | Dark Theme Config Variable |
|
|
||||||
| DARK_LIGHT_SECONDARY | #973542 | Dark Theme Config Variable |
|
### Email
|
||||||
| DARK_LIGHT_SUCCESS | #43A047 | Dark Theme Config Variable |
|
|
||||||
| DARK_LIGHT_INFO | #1976D2 | Dark Theme Config Variable |
|
| Variables | Default | Description |
|
||||||
| DARK_LIGHT_WARNING | #FF6D00 | Dark Theme Config Variable |
|
| --------------- | :-----: | ------------------ |
|
||||||
| DARK_LIGHT_ERROR | #EF5350 | Dark Theme Config Variable |
|
| SMTP_HOST | None | Required For email |
|
||||||
|
| SMTP_PORT | 587 | Required For email |
|
||||||
|
| SMTP_FROM_NAME | Mealie | Required For email |
|
||||||
|
| SMTP_TLS | true | Required For email |
|
||||||
|
| SMTP_FROM_EMAIL | None | Required For email |
|
||||||
|
| SMTP_USER | None | Required For email |
|
||||||
|
| SMTP_PASSWORD | None | Required For email |
|
||||||
|
|
||||||
|
### Webworkers
|
||||||
|
| Variables | Default | Description |
|
||||||
|
| ---------------- | :-----: | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| WORKERS_PER_CORE | 1 | Set the number of workers to the number of CPU cores multiplied by this value (Value \* CPUs). More info [here][workers_per_core] |
|
||||||
|
| MAX_WORKERS | | Set the maximum number of workers to use. Default is not set meaning unlimited. More info [here][max_workers] |
|
||||||
|
| WEB_CONCURRENCY | 2 | Override the automatic definition of number of workers. More info [here][web_concurrency] |
|
||||||
|
|
||||||
|
|
||||||
|
## Frontend Environment Variables
|
||||||
|
|
||||||
|
### General
|
||||||
|
|
||||||
|
| Variables | Default | Description |
|
||||||
|
| ------------ | :-----: | ---------------------------------- |
|
||||||
|
| ALLOW_SIGNUP | true | Allows anyone to signup for Mealie |
|
||||||
|
|
||||||
|
## Themeing
|
||||||
|
| Variables | Default | Description |
|
||||||
|
| --------------------- | :-----: | --------------------------- |
|
||||||
|
| THEME_LIGHT_PRIMARY | #E58325 | Light Theme Config Variable |
|
||||||
|
| THEME_LIGHT_ACCENT | #007A99 | Light Theme Config Variable |
|
||||||
|
| THEME_LIGHT_SECONDARY | #973542 | Light Theme Config Variable |
|
||||||
|
| THEME_LIGHT_SUCCESS | #43A047 | Light Theme Config Variable |
|
||||||
|
| THEME_LIGHT_INFO | #1976D2 | Light Theme Config Variable |
|
||||||
|
| THEME_LIGHT_WARNING | #FF6D00 | Light Theme Config Variable |
|
||||||
|
| THEME_LIGHT_ERROR | #EF5350 | Light Theme Config Variable |
|
||||||
|
| DARK_LIGHT_PRIMARY | #E58325 | Dark Theme Config Variable |
|
||||||
|
| DARK_LIGHT_ACCENT | #007A99 | Dark Theme Config Variable |
|
||||||
|
| DARK_LIGHT_SECONDARY | #973542 | Dark Theme Config Variable |
|
||||||
|
| DARK_LIGHT_SUCCESS | #43A047 | Dark Theme Config Variable |
|
||||||
|
| DARK_LIGHT_INFO | #1976D2 | Dark Theme Config Variable |
|
||||||
|
| DARK_LIGHT_WARNING | #FF6D00 | Dark Theme Config Variable |
|
||||||
|
| DARK_LIGHT_ERROR | #EF5350 | Dark Theme Config Variable |
|
||||||
|
|
||||||
## Raspberry Pi 4
|
## Raspberry Pi 4
|
||||||
|
|
||||||
|
File diff suppressed because one or more lines are too long
28
frontend/api/class-interfaces/email.ts
Normal file
28
frontend/api/class-interfaces/email.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { BaseAPI } from "./_base";
|
||||||
|
|
||||||
|
const routes = {
|
||||||
|
base: "/api/admin/email",
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface CheckEmailResponse {
|
||||||
|
ready: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TestEmailResponse {
|
||||||
|
success: boolean;
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TestEmailPayload {
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EmailAPI extends BaseAPI {
|
||||||
|
check() {
|
||||||
|
return this.requests.get<CheckEmailResponse>(routes.base);
|
||||||
|
}
|
||||||
|
|
||||||
|
test(payload: TestEmailPayload) {
|
||||||
|
return this.requests.post<TestEmailResponse>(routes.base, payload);
|
||||||
|
}
|
||||||
|
}
|
@ -15,6 +15,7 @@ import { WebhooksAPI } from "./class-interfaces/group-webhooks";
|
|||||||
import { AdminAboutAPI } from "./class-interfaces/admin-about";
|
import { AdminAboutAPI } from "./class-interfaces/admin-about";
|
||||||
import { RegisterAPI } from "./class-interfaces/user-registration";
|
import { RegisterAPI } from "./class-interfaces/user-registration";
|
||||||
import { MealPlanAPI } from "./class-interfaces/group-mealplan";
|
import { MealPlanAPI } from "./class-interfaces/group-mealplan";
|
||||||
|
import { EmailAPI } from "./class-interfaces/email";
|
||||||
import { ApiRequestInstance } from "~/types/api";
|
import { ApiRequestInstance } from "~/types/api";
|
||||||
|
|
||||||
class AdminAPI {
|
class AdminAPI {
|
||||||
@ -50,6 +51,7 @@ class Api {
|
|||||||
public groupWebhooks: WebhooksAPI;
|
public groupWebhooks: WebhooksAPI;
|
||||||
public register: RegisterAPI;
|
public register: RegisterAPI;
|
||||||
public mealplans: MealPlanAPI;
|
public mealplans: MealPlanAPI;
|
||||||
|
public email: EmailAPI;
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
public upload: UploadFile;
|
public upload: UploadFile;
|
||||||
@ -83,6 +85,8 @@ class Api {
|
|||||||
this.upload = new UploadFile(requests);
|
this.upload = new UploadFile(requests);
|
||||||
this.utils = new UtilsAPI(requests);
|
this.utils = new UtilsAPI(requests);
|
||||||
|
|
||||||
|
this.email = new EmailAPI(requests);
|
||||||
|
|
||||||
Object.freeze(this);
|
Object.freeze(this);
|
||||||
Api.instance = this;
|
Api.instance = this;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-card flat class="pb-2">
|
<v-card flat class="pb-2">
|
||||||
<h2 class="headline">{{ title }}</h2>
|
<v-card-title class="headline py-0">
|
||||||
<!-- <BaseDivider width="200px" color="primary" class="my-2" thickness="1px" /> -->
|
<v-icon v-if="icon !== ''" left>
|
||||||
|
{{ icon }}
|
||||||
|
</v-icon>
|
||||||
|
{{ title }}
|
||||||
|
</v-card-title>
|
||||||
<p class="pb-0 mb-0">
|
<p class="pb-0 mb-0">
|
||||||
<slot />
|
<slot />
|
||||||
</p>
|
</p>
|
||||||
@ -16,6 +20,10 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
default: "Place Holder",
|
default: "Place Holder",
|
||||||
},
|
},
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,21 +1,127 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-container fluid>
|
<v-container fluid class="narrow-container">
|
||||||
<BaseCardSectionTitle title="Sitewide Settings">
|
<BasePageTitle divider>
|
||||||
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Alias error provident, eveniet, laboriosam assumenda
|
<template #header>
|
||||||
earum amet quaerat vel consequatur molestias sed enim. Adipisci a consequuntur dolor culpa expedita voluptatem
|
<v-img
|
||||||
praesentium optio iste atque, ea reiciendis iure non aut suscipit modi ducimus ratione, quam numquam quaerat
|
max-height="200"
|
||||||
distinctio illum nemo. Dicta, doloremque!
|
max-width="150"
|
||||||
</BaseCardSectionTitle>
|
class="mb-2"
|
||||||
|
:src="require('~/static/svgs/admin-site-settings.svg')"
|
||||||
|
></v-img>
|
||||||
|
</template>
|
||||||
|
<template #title> {{ $t("settings.site-settings") }} </template>
|
||||||
|
</BasePageTitle>
|
||||||
|
<BaseCardSectionTitle :icon="$globals.icons.email" title="Email Configuration"> </BaseCardSectionTitle>
|
||||||
|
<v-card>
|
||||||
|
<v-card-text>
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-avatar>
|
||||||
|
<v-icon :color="ready ? 'success' : 'error'">
|
||||||
|
{{ ready ? $globals.icons.check : $globals.icons.close }}
|
||||||
|
</v-icon>
|
||||||
|
</v-list-item-avatar>
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title
|
||||||
|
:class="{
|
||||||
|
'success--text': ready,
|
||||||
|
'error--text': !ready,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
Email Configuration Status
|
||||||
|
</v-list-item-title>
|
||||||
|
<v-list-item-subtitle
|
||||||
|
:class="{
|
||||||
|
'success--text': ready,
|
||||||
|
'error--text': !ready,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ ready ? "Ready" : "Not Ready - Check Env Variables" }}
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-text-field v-model="address" class="mr-4" :label="$t('user.email')" :rules="[validators.email]">
|
||||||
|
</v-text-field>
|
||||||
|
<BaseButton color="info" :disabled="!ready || !validEmail" :loading="loading" @click="testEmail">
|
||||||
|
<template #icon> {{ $globals.icons.email }} </template>
|
||||||
|
{{ $t("general.test") }}
|
||||||
|
</BaseButton>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card-text>
|
||||||
|
<template v-if="tested">
|
||||||
|
<v-divider class="my-x"></v-divider>
|
||||||
|
<v-card-text>
|
||||||
|
Email Test Result: {{ success ? "Succeeded" : "Failed" }}
|
||||||
|
<div>Errors: {{ error }}</div>
|
||||||
|
</v-card-text>
|
||||||
|
</template>
|
||||||
|
</v-card>
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from "@nuxtjs/composition-api";
|
import { computed, defineComponent, onMounted, reactive, toRefs } from "@nuxtjs/composition-api";
|
||||||
|
import { useApiSingleton } from "~/composables/use-api";
|
||||||
|
import { validators } from "~/composables/use-validators";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
layout: "admin",
|
layout: "admin",
|
||||||
setup() {
|
setup() {
|
||||||
return {};
|
const state = reactive({
|
||||||
|
ready: true,
|
||||||
|
loading: false,
|
||||||
|
address: "",
|
||||||
|
success: false,
|
||||||
|
error: "",
|
||||||
|
tested: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const api = useApiSingleton();
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const { data } = await api.email.check();
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
state.ready = data.ready;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function testEmail() {
|
||||||
|
state.loading = true;
|
||||||
|
state.tested = false;
|
||||||
|
const { data } = await api.email.test({ email: state.address });
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
if (data.success) {
|
||||||
|
state.success = true;
|
||||||
|
} else {
|
||||||
|
state.error = data.error;
|
||||||
|
state.success = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.loading = false;
|
||||||
|
state.tested = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validEmail = computed(() => {
|
||||||
|
if (state.address === "") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const valid = validators.email(state.address);
|
||||||
|
|
||||||
|
// Explicit bool check because validators.email sometimes returns a string
|
||||||
|
if (valid === true) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
validEmail,
|
||||||
|
validators,
|
||||||
|
...toRefs(state),
|
||||||
|
testEmail,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
1
frontend/static/svgs/admin-site-settings.svg
Normal file
1
frontend/static/svgs/admin-site-settings.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 21 KiB |
@ -156,10 +156,6 @@ class AppSettings(BaseSettings):
|
|||||||
|
|
||||||
TOKEN_TIME: int = 2 # Time in Hours
|
TOKEN_TIME: int = 2 # Time in Hours
|
||||||
|
|
||||||
# Not Used!
|
|
||||||
SFTP_USERNAME: Optional[str]
|
|
||||||
SFTP_PASSWORD: Optional[str]
|
|
||||||
|
|
||||||
# Recipe Default Settings
|
# Recipe Default Settings
|
||||||
RECIPE_PUBLIC: bool = True
|
RECIPE_PUBLIC: bool = True
|
||||||
RECIPE_SHOW_NUTRITION: bool = True
|
RECIPE_SHOW_NUTRITION: bool = True
|
||||||
@ -168,6 +164,31 @@ class AppSettings(BaseSettings):
|
|||||||
RECIPE_DISABLE_COMMENTS: bool = False
|
RECIPE_DISABLE_COMMENTS: bool = False
|
||||||
RECIPE_DISABLE_AMOUNT: bool = False
|
RECIPE_DISABLE_AMOUNT: bool = False
|
||||||
|
|
||||||
|
# ===============================================
|
||||||
|
# Email Configuration
|
||||||
|
SMTP_HOST: Optional[str]
|
||||||
|
SMTP_PORT: Optional[str] = "587"
|
||||||
|
SMTP_FROM_NAME: Optional[str] = "Mealie"
|
||||||
|
SMTP_TLS: Optional[bool] = True
|
||||||
|
SMTP_FROM_EMAIL: Optional[str]
|
||||||
|
SMTP_USER: Optional[str]
|
||||||
|
SMTP_PASSWORD: Optional[str]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def SMTP_ENABLE(self) -> bool:
|
||||||
|
"""Validates all SMTP variables are set"""
|
||||||
|
required = {
|
||||||
|
self.SMTP_HOST,
|
||||||
|
self.SMTP_PORT,
|
||||||
|
self.SMTP_FROM_NAME,
|
||||||
|
self.SMTP_TLS,
|
||||||
|
self.SMTP_FROM_EMAIL,
|
||||||
|
self.SMTP_USER,
|
||||||
|
self.SMTP_PASSWORD,
|
||||||
|
}
|
||||||
|
|
||||||
|
return "" not in required and None not in required
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
env_file = BASE_DIR.joinpath(".env")
|
env_file = BASE_DIR.joinpath(".env")
|
||||||
env_file_encoding = "utf-8"
|
env_file_encoding = "utf-8"
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from . import admin_about, admin_group, admin_log
|
from mealie.routes.routers import AdminAPIRouter
|
||||||
|
|
||||||
router = APIRouter(prefix="/admin")
|
from . import admin_about, admin_email, admin_group, admin_log
|
||||||
|
|
||||||
|
router = AdminAPIRouter(prefix="/admin")
|
||||||
|
|
||||||
router.include_router(admin_about.router, tags=["Admin: About"])
|
router.include_router(admin_about.router, tags=["Admin: About"])
|
||||||
router.include_router(admin_log.router, tags=["Admin: Log"])
|
router.include_router(admin_log.router, tags=["Admin: Log"])
|
||||||
router.include_router(admin_group.router, tags=["Admin: Group"])
|
router.include_router(admin_group.router, tags=["Admin: Group"])
|
||||||
|
router.include_router(admin_email.router, tags=["Admin: Email"])
|
||||||
|
47
mealie/routes/admin/admin_email.py
Normal file
47
mealie/routes/admin/admin_email.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
from fastapi_camelcase import CamelModel
|
||||||
|
|
||||||
|
from mealie.core.config import get_settings
|
||||||
|
from mealie.core.root_logger import get_logger
|
||||||
|
from mealie.services.email import EmailService
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/email")
|
||||||
|
|
||||||
|
|
||||||
|
class EmailReady(CamelModel):
|
||||||
|
ready: bool
|
||||||
|
|
||||||
|
|
||||||
|
class EmailSuccess(CamelModel):
|
||||||
|
success: bool
|
||||||
|
error: str = None
|
||||||
|
|
||||||
|
|
||||||
|
class EmailTest(CamelModel):
|
||||||
|
email: str
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("", response_model=EmailReady)
|
||||||
|
async def check_email_config():
|
||||||
|
""" Get general application information """
|
||||||
|
settings = get_settings()
|
||||||
|
|
||||||
|
return EmailReady(ready=settings.SMTP_ENABLE)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("", response_model=EmailSuccess)
|
||||||
|
async def send_test_email(data: EmailTest):
|
||||||
|
print(data)
|
||||||
|
service = EmailService()
|
||||||
|
status = False
|
||||||
|
error = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
status = service.send_test_email(data.email)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
error = str(e)
|
||||||
|
|
||||||
|
return EmailSuccess(success=status, error=error)
|
1
mealie/services/email/__init__.py
Normal file
1
mealie/services/email/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .email_service import EmailService, EmailTemplate
|
35
mealie/services/email/email_senders.py
Normal file
35
mealie/services/email/email_senders.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
import emails
|
||||||
|
|
||||||
|
from mealie.core.root_logger import get_logger
|
||||||
|
from mealie.services._base_service import BaseService
|
||||||
|
|
||||||
|
logger = get_logger()
|
||||||
|
|
||||||
|
|
||||||
|
class ABCEmailSender(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def send(self, email_to: str, subject: str, html: str) -> bool:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class DefaultEmailSender(ABCEmailSender, BaseService):
|
||||||
|
def send(self, email_to: str, subject: str, html: str) -> bool:
|
||||||
|
message = emails.Message(
|
||||||
|
subject=subject,
|
||||||
|
html=html,
|
||||||
|
mail_from=(self.settings.SMTP_FROM_NAME, self.settings.SMTP_FROM_EMAIL),
|
||||||
|
)
|
||||||
|
|
||||||
|
smtp_options = {"host": self.settings.SMTP_HOST, "port": self.settings.SMTP_PORT}
|
||||||
|
if self.settings.SMTP_TLS:
|
||||||
|
smtp_options["tls"] = True
|
||||||
|
if self.settings.SMTP_USER:
|
||||||
|
smtp_options["user"] = self.settings.SMTP_USER
|
||||||
|
if self.settings.SMTP_PASSWORD:
|
||||||
|
smtp_options["password"] = self.settings.SMTP_PASSWORD
|
||||||
|
response = message.send(to=email_to, smtp=smtp_options)
|
||||||
|
logger.info(f"send email result: {response}")
|
||||||
|
|
||||||
|
return response.status_code in [250]
|
86
mealie/services/email/email_service.py
Normal file
86
mealie/services/email/email_service.py
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from jinja2 import Template
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from mealie.core.root_logger import get_logger
|
||||||
|
from mealie.services._base_service import BaseService
|
||||||
|
|
||||||
|
from .email_senders import ABCEmailSender, DefaultEmailSender
|
||||||
|
|
||||||
|
CWD = Path(__file__).parent
|
||||||
|
|
||||||
|
logger = get_logger()
|
||||||
|
|
||||||
|
|
||||||
|
class EmailTemplate(BaseModel):
|
||||||
|
subject: str
|
||||||
|
header_text: str
|
||||||
|
message_top: str
|
||||||
|
message_bottom: str
|
||||||
|
button_link: str
|
||||||
|
button_text: str
|
||||||
|
|
||||||
|
def render_html(self, template: Path) -> str:
|
||||||
|
tmpl = Template(template.read_text())
|
||||||
|
|
||||||
|
return tmpl.render(data=self.dict())
|
||||||
|
|
||||||
|
|
||||||
|
class EmailService(BaseService):
|
||||||
|
def __init__(self, sender: ABCEmailSender = None) -> None:
|
||||||
|
self.templates_dir = CWD / "templates"
|
||||||
|
self.default_template = self.templates_dir / "default.html"
|
||||||
|
self.sender: ABCEmailSender = sender or DefaultEmailSender()
|
||||||
|
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def send_email(self, email_to: str, data: EmailTemplate) -> bool:
|
||||||
|
if not self.settings.SMTP_ENABLE:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return self.sender.send(email_to, data.subject, data.render_html(self.default_template))
|
||||||
|
|
||||||
|
def send_forgot_password(self, address: str, reset_password_url: str) -> bool:
|
||||||
|
forgot_password = EmailTemplate(
|
||||||
|
subject="Mealie Forgot Password",
|
||||||
|
header_text="Forgot Password",
|
||||||
|
message_top="You have requested to reset your password.",
|
||||||
|
message_bottom="Please click the button below to reset your password.",
|
||||||
|
button_link=reset_password_url,
|
||||||
|
button_text="Reset Password",
|
||||||
|
)
|
||||||
|
return self.send_email(address, forgot_password)
|
||||||
|
|
||||||
|
def send_invitation(self, address: str, invitation_url: str) -> bool:
|
||||||
|
invitation = EmailTemplate(
|
||||||
|
subject="Invitation to join Mealie",
|
||||||
|
header_text="Invitation",
|
||||||
|
message_top="You have been invited to join Mealie.",
|
||||||
|
message_bottom="Please click the button below to accept the invitation.",
|
||||||
|
button_link=invitation_url,
|
||||||
|
button_text="Accept Invitation",
|
||||||
|
)
|
||||||
|
return self.send_email(address, invitation)
|
||||||
|
|
||||||
|
def send_test_email(self, address: str) -> bool:
|
||||||
|
test_email = EmailTemplate(
|
||||||
|
subject="Test Email",
|
||||||
|
header_text="Test Email",
|
||||||
|
message_top="This is a test email.",
|
||||||
|
message_bottom="Please click the button below to test the email.",
|
||||||
|
button_link="https://www.google.com",
|
||||||
|
button_text="Test Email",
|
||||||
|
)
|
||||||
|
return self.send_email(address, test_email)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("Starting...")
|
||||||
|
service = EmailService()
|
||||||
|
service.send_test_email("hay-kot@pm.me")
|
||||||
|
print("Finished...")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
544
mealie/services/email/templates/default.html
Normal file
544
mealie/services/email/templates/default.html
Normal file
@ -0,0 +1,544 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html
|
||||||
|
xmlns="http://www.w3.org/1999/xhtml"
|
||||||
|
xmlns:v="urn:schemas-microsoft-com:vml"
|
||||||
|
xmlns:o="urn:schemas-microsoft-com:office:office"
|
||||||
|
>
|
||||||
|
<head>
|
||||||
|
<title> </title>
|
||||||
|
<!--[if !mso]><!-->
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<!--<![endif]-->
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<style type="text/css">
|
||||||
|
#outlook a {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
-ms-text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
table,
|
||||||
|
td {
|
||||||
|
border-collapse: collapse;
|
||||||
|
mso-table-lspace: 0pt;
|
||||||
|
mso-table-rspace: 0pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
border: 0;
|
||||||
|
height: auto;
|
||||||
|
line-height: 100%;
|
||||||
|
outline: none;
|
||||||
|
text-decoration: none;
|
||||||
|
-ms-interpolation-mode: bicubic;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
display: block;
|
||||||
|
margin: 13px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<!--[if mso]>
|
||||||
|
<noscript>
|
||||||
|
<xml>
|
||||||
|
<o:OfficeDocumentSettings>
|
||||||
|
<o:AllowPNG />
|
||||||
|
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||||
|
</o:OfficeDocumentSettings>
|
||||||
|
</xml>
|
||||||
|
</noscript>
|
||||||
|
<![endif]-->
|
||||||
|
<!--[if lte mso 11]>
|
||||||
|
<style type="text/css">
|
||||||
|
.mj-outlook-group-fix {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<![endif]-->
|
||||||
|
|
||||||
|
<!--[if !mso]><!-->
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700"
|
||||||
|
rel="stylesheet"
|
||||||
|
type="text/css"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700"
|
||||||
|
rel="stylesheet"
|
||||||
|
type="text/css"
|
||||||
|
/>
|
||||||
|
<style type="text/css">
|
||||||
|
@import url(https://fonts.googleapis.com/css?family=Roboto:300,400,500,700);
|
||||||
|
@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);
|
||||||
|
</style>
|
||||||
|
<!--<![endif]-->
|
||||||
|
|
||||||
|
<style type="text/css">
|
||||||
|
@media only screen and (min-width: 480px) {
|
||||||
|
.mj-column-per-100 {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style media="screen and (min-width:480px)">
|
||||||
|
.moz-text-html .mj-column-per-100 {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style type="text/css">
|
||||||
|
@media only screen and (max-width: 480px) {
|
||||||
|
table.mj-full-width-mobile {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.mj-full-width-mobile {
|
||||||
|
width: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
noinput.mj-menu-checkbox {
|
||||||
|
display: block !important;
|
||||||
|
max-height: none !important;
|
||||||
|
visibility: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 480px) {
|
||||||
|
.mj-menu-checkbox[type="checkbox"] ~ .mj-inline-links {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mj-menu-checkbox[type="checkbox"]:checked ~ .mj-inline-links,
|
||||||
|
.mj-menu-checkbox[type="checkbox"] ~ .mj-menu-trigger {
|
||||||
|
display: block !important;
|
||||||
|
max-width: none !important;
|
||||||
|
max-height: none !important;
|
||||||
|
font-size: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mj-menu-checkbox[type="checkbox"] ~ .mj-inline-links > a {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mj-menu-checkbox[type="checkbox"]:checked
|
||||||
|
~ .mj-menu-trigger
|
||||||
|
.mj-menu-icon-close {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mj-menu-checkbox[type="checkbox"]:checked
|
||||||
|
~ .mj-menu-trigger
|
||||||
|
.mj-menu-icon-open {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body style="word-spacing: normal">
|
||||||
|
<div style="">
|
||||||
|
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
|
||||||
|
<div style="margin: 0px auto; max-width: 600px">
|
||||||
|
<table
|
||||||
|
align="center"
|
||||||
|
border="0"
|
||||||
|
cellpadding="0"
|
||||||
|
cellspacing="0"
|
||||||
|
role="presentation"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
style="
|
||||||
|
direction: ltr;
|
||||||
|
font-size: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
text-align: center;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="mj-column-per-100 mj-outlook-group-fix"
|
||||||
|
style="
|
||||||
|
font-size: 0px;
|
||||||
|
text-align: left;
|
||||||
|
direction: ltr;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
width: 100%;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<table
|
||||||
|
border="0"
|
||||||
|
cellpadding="0"
|
||||||
|
cellspacing="0"
|
||||||
|
role="presentation"
|
||||||
|
style="vertical-align: top"
|
||||||
|
width="100%"
|
||||||
|
>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
align="center"
|
||||||
|
style="
|
||||||
|
font-size: 0px;
|
||||||
|
padding: 10px 25px;
|
||||||
|
word-break: break-word;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<table
|
||||||
|
border="0"
|
||||||
|
cellpadding="0"
|
||||||
|
cellspacing="0"
|
||||||
|
role="presentation"
|
||||||
|
style="
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="width: 550px">
|
||||||
|
<img
|
||||||
|
height="auto"
|
||||||
|
src="https://api-test.emailbuilder.top/saemailbuilder/dc23dc82-ffd7-4f4c-b563-94f23db4c2c3/images/256d8bd6-ffde-4bf2-b577-dd8306dae877/file.png"
|
||||||
|
style="
|
||||||
|
border: 0;
|
||||||
|
display: block;
|
||||||
|
outline: none;
|
||||||
|
text-decoration: none;
|
||||||
|
height: auto;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 13px;
|
||||||
|
"
|
||||||
|
width="550"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
|
||||||
|
<div style="margin: 0px auto; max-width: 600px">
|
||||||
|
<table
|
||||||
|
align="center"
|
||||||
|
border="0"
|
||||||
|
cellpadding="0"
|
||||||
|
cellspacing="0"
|
||||||
|
role="presentation"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
style="
|
||||||
|
direction: ltr;
|
||||||
|
font-size: 0px;
|
||||||
|
padding: 20px 0;
|
||||||
|
text-align: center;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="mj-column-per-100 mj-outlook-group-fix"
|
||||||
|
style="
|
||||||
|
font-size: 0px;
|
||||||
|
text-align: left;
|
||||||
|
direction: ltr;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
width: 100%;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<table
|
||||||
|
border="0"
|
||||||
|
cellpadding="0"
|
||||||
|
cellspacing="0"
|
||||||
|
role="presentation"
|
||||||
|
style="vertical-align: top"
|
||||||
|
width="100%"
|
||||||
|
>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
align="center"
|
||||||
|
style="
|
||||||
|
font-size: 0px;
|
||||||
|
padding: 0px 25px;
|
||||||
|
word-break: break-word;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
font-family: Roboto, Helvetica Neue, Helvetica,
|
||||||
|
Arial, sans-serif;
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 1;
|
||||||
|
text-align: center;
|
||||||
|
color: #e38333;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<h2>{{ data.header_text }}</h2>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
align="left"
|
||||||
|
style="
|
||||||
|
font-size: 0px;
|
||||||
|
padding: 10px 25px;
|
||||||
|
word-break: break-word;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
font-family: Roboto, Helvetica Neue, Helvetica,
|
||||||
|
Arial, sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1;
|
||||||
|
text-align: left;
|
||||||
|
color: #000000;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div style="text-align: center">
|
||||||
|
<b>Hi there!</b>
|
||||||
|
</div>
|
||||||
|
<div><br /></div>
|
||||||
|
<div style="text-align: center">
|
||||||
|
{{ data.message_top }}
|
||||||
|
</div>
|
||||||
|
<div><br /></div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
align="center"
|
||||||
|
vertical-align="middle"
|
||||||
|
style="
|
||||||
|
font-size: 0px;
|
||||||
|
padding: 10px 25px;
|
||||||
|
word-break: break-word;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<table
|
||||||
|
border="0"
|
||||||
|
cellpadding="0"
|
||||||
|
cellspacing="0"
|
||||||
|
role="presentation"
|
||||||
|
style="border-collapse: separate; line-height: 100%"
|
||||||
|
>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
align="center"
|
||||||
|
bgcolor="#e38333"
|
||||||
|
role="presentation"
|
||||||
|
style="
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: auto;
|
||||||
|
mso-padding-alt: 10px 15px;
|
||||||
|
background: #e38333;
|
||||||
|
"
|
||||||
|
valign="middle"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="{{ data.button_link }}"
|
||||||
|
style="
|
||||||
|
display: inline-block;
|
||||||
|
background: #e38333;
|
||||||
|
color: #ffffff;
|
||||||
|
font-family: Ubuntu, Helvetica, Arial,
|
||||||
|
sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 120%;
|
||||||
|
margin: 0;
|
||||||
|
text-decoration: none;
|
||||||
|
text-transform: none;
|
||||||
|
padding: 10px 15px;
|
||||||
|
mso-padding-alt: 0px;
|
||||||
|
border-radius: 3px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ data.button_text}}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
align="left"
|
||||||
|
style="
|
||||||
|
font-size: 0px;
|
||||||
|
padding: 10px 25px;
|
||||||
|
word-break: break-word;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
font-family: Roboto, Helvetica Neue, Helvetica,
|
||||||
|
Arial, sans-serif;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1.5;
|
||||||
|
text-align: left;
|
||||||
|
color: #000000;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div style="text-align: center">
|
||||||
|
{{ data.bottom_message}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
align="center"
|
||||||
|
style="
|
||||||
|
font-size: 0px;
|
||||||
|
padding: 10px 25px;
|
||||||
|
word-break: break-word;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<table
|
||||||
|
border="0"
|
||||||
|
cellpadding="0"
|
||||||
|
cellspacing="0"
|
||||||
|
role="presentation"
|
||||||
|
style="
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0px;
|
||||||
|
"
|
||||||
|
></table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
align="center"
|
||||||
|
style="
|
||||||
|
font-size: 0px;
|
||||||
|
padding: 5px 0px;
|
||||||
|
word-break: break-word;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="mj-inline-links" style="">
|
||||||
|
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0" align="center"><tr><td style="padding:15px 10px;" class="" ><![endif]-->
|
||||||
|
|
||||||
|
<a
|
||||||
|
class="mj-link"
|
||||||
|
href="https://github.com/hay-kot/mealie"
|
||||||
|
target="_blank"
|
||||||
|
style="
|
||||||
|
display: inline-block;
|
||||||
|
color: #dd8333;
|
||||||
|
font-family: Roboto, Helvetica Neue, Helvetica,
|
||||||
|
Arial, sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: 22px;
|
||||||
|
text-decoration: none;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 15px 10px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Github
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!--[if mso | IE]></td><td style="padding:15px 10px;" class="" ><![endif]-->
|
||||||
|
|
||||||
|
<a
|
||||||
|
class="mj-link"
|
||||||
|
href="https://discord.gg/PfByzb5EKH"
|
||||||
|
target="_blank"
|
||||||
|
style="
|
||||||
|
display: inline-block;
|
||||||
|
color: #dd8333;
|
||||||
|
font-family: Roboto, Helvetica Neue, Helvetica,
|
||||||
|
Arial, sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: 22px;
|
||||||
|
text-decoration: none;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 15px 10px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Discord
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!--[if mso | IE]></td><td style="padding:15px 10px;" class="" ><![endif]-->
|
||||||
|
|
||||||
|
<a
|
||||||
|
class="mj-link"
|
||||||
|
href="https://hay-kot.github.io/mealie/"
|
||||||
|
target="_blank"
|
||||||
|
style="
|
||||||
|
display: inline-block;
|
||||||
|
color: #dd8333;
|
||||||
|
font-family: Roboto, Helvetica Neue, Helvetica,
|
||||||
|
Arial, sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: 22px;
|
||||||
|
text-decoration: none;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 15px 10px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Documentation
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
99
poetry.lock
generated
99
poetry.lock
generated
@ -153,6 +153,14 @@ typing-extensions = ">=3.7.4"
|
|||||||
colorama = ["colorama (>=0.4.3)"]
|
colorama = ["colorama (>=0.4.3)"]
|
||||||
d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
|
d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cachetools"
|
||||||
|
version = "4.2.4"
|
||||||
|
description = "Extensible memoizing collections and decorators"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = "~=3.5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "certifi"
|
name = "certifi"
|
||||||
version = "2021.5.30"
|
version = "2021.5.30"
|
||||||
@ -172,6 +180,14 @@ python-versions = "*"
|
|||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
pycparser = "*"
|
pycparser = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chardet"
|
||||||
|
version = "4.0.0"
|
||||||
|
description = "Universal encoding detector for Python 2 and 3"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "charset-normalizer"
|
name = "charset-normalizer"
|
||||||
version = "2.0.6"
|
version = "2.0.6"
|
||||||
@ -240,6 +256,26 @@ sdist = ["setuptools-rust (>=0.11.4)"]
|
|||||||
ssh = ["bcrypt (>=3.1.5)"]
|
ssh = ["bcrypt (>=3.1.5)"]
|
||||||
test = ["pytest (>=6.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"]
|
test = ["pytest (>=6.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cssselect"
|
||||||
|
version = "1.1.0"
|
||||||
|
description = "cssselect parses CSS3 Selectors and translates them to XPath 1.0"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cssutils"
|
||||||
|
version = "2.3.0"
|
||||||
|
description = "A CSS Cascading Style Sheets library for Python"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
|
||||||
|
testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "mock", "lxml", "cssselect", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "decorator"
|
name = "decorator"
|
||||||
version = "5.1.0"
|
version = "5.1.0"
|
||||||
@ -263,6 +299,22 @@ six = ">=1.9.0"
|
|||||||
gmpy = ["gmpy"]
|
gmpy = ["gmpy"]
|
||||||
gmpy2 = ["gmpy2"]
|
gmpy2 = ["gmpy2"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "emails"
|
||||||
|
version = "0.6"
|
||||||
|
description = "Modern python library for emails."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
chardet = "*"
|
||||||
|
cssutils = "*"
|
||||||
|
lxml = "*"
|
||||||
|
premailer = "*"
|
||||||
|
python-dateutil = "*"
|
||||||
|
requests = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "extruct"
|
name = "extruct"
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
@ -707,6 +759,25 @@ python-versions = ">=3.6"
|
|||||||
dev = ["pre-commit", "tox"]
|
dev = ["pre-commit", "tox"]
|
||||||
testing = ["pytest", "pytest-benchmark"]
|
testing = ["pytest", "pytest-benchmark"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "premailer"
|
||||||
|
version = "3.10.0"
|
||||||
|
description = "Turns CSS blocks into style attributes"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
cachetools = "*"
|
||||||
|
cssselect = "*"
|
||||||
|
cssutils = "*"
|
||||||
|
lxml = "*"
|
||||||
|
requests = "*"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["tox", "twine", "therapist", "black", "flake8", "wheel"]
|
||||||
|
test = ["nose", "mock"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "psycopg2-binary"
|
name = "psycopg2-binary"
|
||||||
version = "2.9.1"
|
version = "2.9.1"
|
||||||
@ -887,7 +958,7 @@ testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtuale
|
|||||||
name = "python-dateutil"
|
name = "python-dateutil"
|
||||||
version = "2.8.2"
|
version = "2.8.2"
|
||||||
description = "Extensions to the standard Python datetime module"
|
description = "Extensions to the standard Python datetime module"
|
||||||
category = "dev"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
|
||||||
|
|
||||||
@ -1334,7 +1405,7 @@ pgsql = ["psycopg2-binary"]
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = "^3.9"
|
python-versions = "^3.9"
|
||||||
content-hash = "1b9a18e7114a8f157226c20e951dce0bd08ac884e0795f0f816e9f57d72ec309"
|
content-hash = "c030cae2012cedbcad514df8f63a79288d0390d211cfdf4f5a6489a11c96d923"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
aiofiles = [
|
aiofiles = [
|
||||||
@ -1385,6 +1456,10 @@ beautifulsoup4 = [
|
|||||||
black = [
|
black = [
|
||||||
{file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"},
|
{file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"},
|
||||||
]
|
]
|
||||||
|
cachetools = [
|
||||||
|
{file = "cachetools-4.2.4-py3-none-any.whl", hash = "sha256:92971d3cb7d2a97efff7c7bb1657f21a8f5fb309a37530537c71b1774189f2d1"},
|
||||||
|
{file = "cachetools-4.2.4.tar.gz", hash = "sha256:89ea6f1b638d5a73a4f9226be57ac5e4f399d22770b92355f92dcb0f7f001693"},
|
||||||
|
]
|
||||||
certifi = [
|
certifi = [
|
||||||
{file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"},
|
{file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"},
|
||||||
{file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"},
|
{file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"},
|
||||||
@ -1436,6 +1511,10 @@ cffi = [
|
|||||||
{file = "cffi-1.14.6-cp39-cp39-win_amd64.whl", hash = "sha256:818014c754cd3dba7229c0f5884396264d51ffb87ec86e927ef0be140bfdb0d2"},
|
{file = "cffi-1.14.6-cp39-cp39-win_amd64.whl", hash = "sha256:818014c754cd3dba7229c0f5884396264d51ffb87ec86e927ef0be140bfdb0d2"},
|
||||||
{file = "cffi-1.14.6.tar.gz", hash = "sha256:c9a875ce9d7fe32887784274dd533c57909b7b1dcadcc128a2ac21331a9765dd"},
|
{file = "cffi-1.14.6.tar.gz", hash = "sha256:c9a875ce9d7fe32887784274dd533c57909b7b1dcadcc128a2ac21331a9765dd"},
|
||||||
]
|
]
|
||||||
|
chardet = [
|
||||||
|
{file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"},
|
||||||
|
{file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"},
|
||||||
|
]
|
||||||
charset-normalizer = [
|
charset-normalizer = [
|
||||||
{file = "charset-normalizer-2.0.6.tar.gz", hash = "sha256:5ec46d183433dcbd0ab716f2d7f29d8dee50505b3fdb40c6b985c7c4f5a3591f"},
|
{file = "charset-normalizer-2.0.6.tar.gz", hash = "sha256:5ec46d183433dcbd0ab716f2d7f29d8dee50505b3fdb40c6b985c7c4f5a3591f"},
|
||||||
{file = "charset_normalizer-2.0.6-py3-none-any.whl", hash = "sha256:5d209c0a931f215cee683b6445e2d77677e7e75e159f78def0db09d68fafcaa6"},
|
{file = "charset_normalizer-2.0.6-py3-none-any.whl", hash = "sha256:5d209c0a931f215cee683b6445e2d77677e7e75e159f78def0db09d68fafcaa6"},
|
||||||
@ -1525,6 +1604,14 @@ cryptography = [
|
|||||||
{file = "cryptography-3.4.8-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:cd65b60cfe004790c795cc35f272e41a3df4631e2fb6b35aa7ac6ef2859d554e"},
|
{file = "cryptography-3.4.8-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:cd65b60cfe004790c795cc35f272e41a3df4631e2fb6b35aa7ac6ef2859d554e"},
|
||||||
{file = "cryptography-3.4.8.tar.gz", hash = "sha256:94cc5ed4ceaefcbe5bf38c8fba6a21fc1d365bb8fb826ea1688e3370b2e24a1c"},
|
{file = "cryptography-3.4.8.tar.gz", hash = "sha256:94cc5ed4ceaefcbe5bf38c8fba6a21fc1d365bb8fb826ea1688e3370b2e24a1c"},
|
||||||
]
|
]
|
||||||
|
cssselect = [
|
||||||
|
{file = "cssselect-1.1.0-py2.py3-none-any.whl", hash = "sha256:f612ee47b749c877ebae5bb77035d8f4202c6ad0f0fc1271b3c18ad6c4468ecf"},
|
||||||
|
{file = "cssselect-1.1.0.tar.gz", hash = "sha256:f95f8dedd925fd8f54edb3d2dfb44c190d9d18512377d3c1e2388d16126879bc"},
|
||||||
|
]
|
||||||
|
cssutils = [
|
||||||
|
{file = "cssutils-2.3.0-py3-none-any.whl", hash = "sha256:0cf1f6086b020dee18048ff3999339499f725934017ef9ae2cd5bb77f9ab5f46"},
|
||||||
|
{file = "cssutils-2.3.0.tar.gz", hash = "sha256:b2d3b16047caae82e5c590036935bafa1b621cf45c2f38885af4be4838f0fd00"},
|
||||||
|
]
|
||||||
decorator = [
|
decorator = [
|
||||||
{file = "decorator-5.1.0-py3-none-any.whl", hash = "sha256:7b12e7c3c6ab203a29e157335e9122cb03de9ab7264b137594103fd4a683b374"},
|
{file = "decorator-5.1.0-py3-none-any.whl", hash = "sha256:7b12e7c3c6ab203a29e157335e9122cb03de9ab7264b137594103fd4a683b374"},
|
||||||
{file = "decorator-5.1.0.tar.gz", hash = "sha256:e59913af105b9860aa2c8d3272d9de5a56a4e608db9a2f167a8480b323d529a7"},
|
{file = "decorator-5.1.0.tar.gz", hash = "sha256:e59913af105b9860aa2c8d3272d9de5a56a4e608db9a2f167a8480b323d529a7"},
|
||||||
@ -1533,6 +1620,10 @@ ecdsa = [
|
|||||||
{file = "ecdsa-0.17.0-py2.py3-none-any.whl", hash = "sha256:5cf31d5b33743abe0dfc28999036c849a69d548f994b535e527ee3cb7f3ef676"},
|
{file = "ecdsa-0.17.0-py2.py3-none-any.whl", hash = "sha256:5cf31d5b33743abe0dfc28999036c849a69d548f994b535e527ee3cb7f3ef676"},
|
||||||
{file = "ecdsa-0.17.0.tar.gz", hash = "sha256:b9f500bb439e4153d0330610f5d26baaf18d17b8ced1bc54410d189385ea68aa"},
|
{file = "ecdsa-0.17.0.tar.gz", hash = "sha256:b9f500bb439e4153d0330610f5d26baaf18d17b8ced1bc54410d189385ea68aa"},
|
||||||
]
|
]
|
||||||
|
emails = [
|
||||||
|
{file = "emails-0.6-py2.py3-none-any.whl", hash = "sha256:72c1e3198075709cc35f67e1b49e2da1a2bc087e9b444073db61a379adfb7f3c"},
|
||||||
|
{file = "emails-0.6.tar.gz", hash = "sha256:a4c2d67ea8b8831967a750d8edc6e77040d7693143fe280e6d2a367d9c36ff88"},
|
||||||
|
]
|
||||||
extruct = [
|
extruct = [
|
||||||
{file = "extruct-0.12.0-py2.py3-none-any.whl", hash = "sha256:42c6c9f50b00aa6c17b5c26b5f1b3a337ebc27b427fafc3714f34ce3bbb16c2f"},
|
{file = "extruct-0.12.0-py2.py3-none-any.whl", hash = "sha256:42c6c9f50b00aa6c17b5c26b5f1b3a337ebc27b427fafc3714f34ce3bbb16c2f"},
|
||||||
{file = "extruct-0.12.0.tar.gz", hash = "sha256:d4a68bb79d1b85ff36d603a42c2666888bb480191a399a659d9daaf735358276"},
|
{file = "extruct-0.12.0.tar.gz", hash = "sha256:d4a68bb79d1b85ff36d603a42c2666888bb480191a399a659d9daaf735358276"},
|
||||||
@ -1876,6 +1967,10 @@ pluggy = [
|
|||||||
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
|
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
|
||||||
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
|
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
|
||||||
]
|
]
|
||||||
|
premailer = [
|
||||||
|
{file = "premailer-3.10.0-py2.py3-none-any.whl", hash = "sha256:021b8196364d7df96d04f9ade51b794d0b77bcc19e998321c515633a2273be1a"},
|
||||||
|
{file = "premailer-3.10.0.tar.gz", hash = "sha256:d1875a8411f5dc92b53ef9f193db6c0f879dc378d618e0ad292723e388bfe4c2"},
|
||||||
|
]
|
||||||
psycopg2-binary = [
|
psycopg2-binary = [
|
||||||
{file = "psycopg2-binary-2.9.1.tar.gz", hash = "sha256:b0221ca5a9837e040ebf61f48899926b5783668b7807419e4adae8175a31f773"},
|
{file = "psycopg2-binary-2.9.1.tar.gz", hash = "sha256:b0221ca5a9837e040ebf61f48899926b5783668b7807419e4adae8175a31f773"},
|
||||||
{file = "psycopg2_binary-2.9.1-cp36-cp36m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:c250a7ec489b652c892e4f0a5d122cc14c3780f9f643e1a326754aedf82d9a76"},
|
{file = "psycopg2_binary-2.9.1-cp36-cp36m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:c250a7ec489b652c892e4f0a5d122cc14c3780f9f643e1a326754aedf82d9a76"},
|
||||||
|
@ -36,6 +36,7 @@ apprise = "0.9.3"
|
|||||||
recipe-scrapers = "^13.2.7"
|
recipe-scrapers = "^13.2.7"
|
||||||
psycopg2-binary = {version = "^2.9.1", optional = true}
|
psycopg2-binary = {version = "^2.9.1", optional = true}
|
||||||
gunicorn = "^20.1.0"
|
gunicorn = "^20.1.0"
|
||||||
|
emails = "^0.6"
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
pylint = "^2.6.0"
|
pylint = "^2.6.0"
|
||||||
|
@ -24,8 +24,13 @@ POSTGRES_DB=mealie
|
|||||||
TOKEN_TIME=24
|
TOKEN_TIME=24
|
||||||
|
|
||||||
# NOT USED
|
# NOT USED
|
||||||
SFTP_USERNAME=None
|
# SMTP_HOST=""
|
||||||
SFTP_PASSWORD=None
|
# SMTP_PORT=""
|
||||||
|
# SMTP_FROM_NAME=""
|
||||||
|
# SMTP_TLS=""
|
||||||
|
# SMTP_FROM_EMAIL=""
|
||||||
|
# SMTP_USER=""
|
||||||
|
# SMTP_PASSWORD=""
|
||||||
|
|
||||||
# Default Recipe Settings
|
# Default Recipe Settings
|
||||||
RECIPE_PUBLIC=False
|
RECIPE_PUBLIC=False
|
||||||
|
@ -77,3 +77,19 @@ def test_set_data_dir():
|
|||||||
|
|
||||||
assert determine_data_dir(True) == PROD_DIR
|
assert determine_data_dir(True) == PROD_DIR
|
||||||
assert determine_data_dir(False) == DEV_DIR
|
assert determine_data_dir(False) == DEV_DIR
|
||||||
|
|
||||||
|
|
||||||
|
def test_smtp_enable(monkeypatch):
|
||||||
|
app_settings = AppSettings()
|
||||||
|
assert app_settings.SMTP_ENABLE is False
|
||||||
|
|
||||||
|
monkeypatch.setenv("SMTP_HOST", "email.mealie.io")
|
||||||
|
monkeypatch.setenv("SMTP_PORT", "587")
|
||||||
|
monkeypatch.setenv("SMTP_TLS", "true")
|
||||||
|
monkeypatch.setenv("SMTP_FROM_NAME", "Mealie")
|
||||||
|
monkeypatch.setenv("SMTP_FROM_EMAIL", "mealie@mealie.io")
|
||||||
|
monkeypatch.setenv("SMTP_USER", "mealie@mealie.io")
|
||||||
|
monkeypatch.setenv("SMTP_PASSWORD", "mealie-password")
|
||||||
|
|
||||||
|
app_settings = AppSettings()
|
||||||
|
assert app_settings.SMTP_ENABLE is True
|
||||||
|
65
tests/unit_tests/test_email_service.py
Normal file
65
tests/unit_tests/test_email_service.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from mealie.core.config import AppSettings
|
||||||
|
from mealie.services.email import EmailService
|
||||||
|
from mealie.services.email.email_senders import ABCEmailSender
|
||||||
|
|
||||||
|
FAKE_ADDRESS = "my_secret_email@email.com"
|
||||||
|
|
||||||
|
SUBJECTS = {"Mealie Forgot Password", "Invitation to join Mealie", "Test Email"}
|
||||||
|
|
||||||
|
|
||||||
|
class TestEmailSender(ABCEmailSender):
|
||||||
|
def send(self, email_to: str, subject: str, html: str) -> bool:
|
||||||
|
|
||||||
|
# check email_to:
|
||||||
|
assert email_to == FAKE_ADDRESS
|
||||||
|
|
||||||
|
# check subject:
|
||||||
|
assert subject in SUBJECTS
|
||||||
|
|
||||||
|
# check html is rendered:
|
||||||
|
assert "{{" not in html
|
||||||
|
assert "}}" not in html
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def patch_env(monkeypatch):
|
||||||
|
monkeypatch.setenv("SMTP_HOST", "email.mealie.io")
|
||||||
|
monkeypatch.setenv("SMTP_PORT", 587)
|
||||||
|
monkeypatch.setenv("SMTP_TLS", True)
|
||||||
|
monkeypatch.setenv("SMTP_FROM_NAME", "Mealie")
|
||||||
|
monkeypatch.setenv("SMTP_FROM_EMAIL", "mealie@mealie.io")
|
||||||
|
monkeypatch.setenv("SMTP_USER", "mealie@mealie.io")
|
||||||
|
monkeypatch.setenv("SMTP_PASSWORD", "mealie-password")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def email_service(monkeypatch) -> EmailService:
|
||||||
|
patch_env(monkeypatch)
|
||||||
|
email_service = EmailService(TestEmailSender())
|
||||||
|
email_service.settings = AppSettings()
|
||||||
|
return email_service
|
||||||
|
|
||||||
|
|
||||||
|
def test_email_disabled():
|
||||||
|
email_service = EmailService(TestEmailSender())
|
||||||
|
email_service.settings = AppSettings()
|
||||||
|
success = email_service.send_test_email(FAKE_ADDRESS)
|
||||||
|
assert not success
|
||||||
|
|
||||||
|
|
||||||
|
def test_test_email(email_service):
|
||||||
|
success = email_service.send_test_email(FAKE_ADDRESS)
|
||||||
|
assert success
|
||||||
|
|
||||||
|
|
||||||
|
def test_forgot_password_email(email_service):
|
||||||
|
success = email_service.send_forgot_password(FAKE_ADDRESS, "https://password-url.com")
|
||||||
|
assert success
|
||||||
|
|
||||||
|
|
||||||
|
def test_invitation_email(email_service):
|
||||||
|
success = email_service.send_invitation(FAKE_ADDRESS, "https://invitie-url.com")
|
||||||
|
assert success
|
Loading…
x
Reference in New Issue
Block a user