mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-31 02:03:35 -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:
		| @@ -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,34 +112,65 @@ services: | |||||||
|       POSTGRES_USER: mealie |       POSTGRES_USER: mealie | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| ## mealie-api Env Variables | ## API Environment Variables | ||||||
|  |  | ||||||
|  | ### General | ||||||
|  |  | ||||||
| | Variables     |        Default        | Description                                                                         | | | Variables     |        Default        | Description                                                                         | | ||||||
| | ----------------- | :-------------------: | --------------------------------------------------------------------------------------------------------------------------------- | | | ------------- | :-------------------: | ----------------------------------------------------------------------------------- | | ||||||
| | PUID          |          911          | UserID permissions between host OS and container                                    | | | PUID          |          911          | UserID permissions between host OS and container                                    | | ||||||
| | PGID          |          911          | GroupID permissions between host OS and container                                   | | | PGID          |          911          | GroupID permissions between host OS and container                                   | | ||||||
| | DEFAULT_GROUP |         Home          | The default group for users                                                         | | | DEFAULT_GROUP |         Home          | The default group for users                                                         | | ||||||
| | DEFAULT_EMAIL |  changeme@email.com   | The default username for the superuser                                              | | | DEFAULT_EMAIL |  changeme@email.com   | The default username for the superuser                                              | | ||||||
| | BASE_URL      | http://localhost:8080 | Used for Notifications                                                              | | | 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                                  | | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### Database | ||||||
|  |  | ||||||
|  | | Variables         | Default  | Description                      | | ||||||
|  | | ----------------- | :------: | -------------------------------- | | ||||||
| | DB_ENGINE         |  sqlite  | Optional: 'sqlite', 'postgres'   | | | DB_ENGINE         |  sqlite  | Optional: 'sqlite', 'postgres'   | | ||||||
| | POSTGRES_USER     |  mealie  | Postgres database user           | | | POSTGRES_USER     |  mealie  | Postgres database user           | | ||||||
| | POSTGRES_PASSWORD |  mealie  | Postgres database password       | | | POSTGRES_PASSWORD |  mealie  | Postgres database password       | | ||||||
| | POSTGRES_SERVER   | postgres | Postgres database server address | | | POSTGRES_SERVER   | postgres | Postgres database server address | | ||||||
| | POSTGRES_PORT     |   5432   | Postgres database port           | | | POSTGRES_PORT     |   5432   | Postgres database port           | | ||||||
| | POSTGRES_DB       |  mealie  | Postgres database name           | | | 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**                                               | | ### Email | ||||||
| | 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        | | ||||||
|  | | --------------- | :-----: | ------------------ | | ||||||
|  | | 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] | | | 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]                     | | | 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]                                         | | | WEB_CONCURRENCY  |    2    | Override the automatic definition of number of workers. More info [here][web_concurrency]                                         | | ||||||
|  |  | ||||||
| ## mealie-frontend Env Variables |  | ||||||
|  | ## Frontend Environment Variables | ||||||
|  |  | ||||||
|  | ### General | ||||||
|  |  | ||||||
| | Variables    | Default | Description                        | | | Variables    | Default | Description                        | | ||||||
| | --------------------- | :-----: | ---------------------------------- | | | ------------ | :-----: | ---------------------------------- | | ||||||
| | ALLOW_SIGNUP |  true   | Allows anyone to signup for Mealie | | | ALLOW_SIGNUP |  true   | Allows anyone to signup for Mealie | | ||||||
|  |  | ||||||
|  | ## Themeing  | ||||||
|  | | Variables             | Default | Description                 | | ||||||
|  | | --------------------- | :-----: | --------------------------- | | ||||||
| | THEME_LIGHT_PRIMARY   | #E58325 | Light Theme Config Variable | | | THEME_LIGHT_PRIMARY   | #E58325 | Light Theme Config Variable | | ||||||
| | THEME_LIGHT_ACCENT    | #007A99 | Light Theme Config Variable | | | THEME_LIGHT_ACCENT    | #007A99 | Light Theme Config Variable | | ||||||
| | THEME_LIGHT_SECONDARY | #973542 | Light Theme Config Variable | | | THEME_LIGHT_SECONDARY | #973542 | Light Theme Config Variable | | ||||||
|   | |||||||
										
											
												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 | ||||||
		Reference in New Issue
	
	Block a user