Initial commit
ci/woodpecker/manual/woodpecker Pipeline failed Details

master
Raoul Snyman 2023-07-31 22:35:39 -07:00
commit 49ce0b5543
19 changed files with 337 additions and 0 deletions

2
.flake8 100644
View File

@ -0,0 +1,2 @@
[flake8]
max-line-length = 120

2
.gitignore vendored 100644
View File

@ -0,0 +1,2 @@
__pycache__
.coverage

53
.woodpecker.yaml 100644
View File

@ -0,0 +1,53 @@
steps:
lint:
image: python:3.11
commands:
- pip install hatch
- hatch run lint
test:
image: python:3.11
commands:
- pip install hatch
- hatch run test
publish-package:
image: python:3.11
commands:
- pip install hatch
- hatch build
- hatch publish --repo $PIP_REPOSITORY --user $PIP_USERNAME --auth $PIP_TOKEN --no-prompt
secrets:
- pip_repository
- pip_username
- pip_token
when:
event: tag
nightly-image:
image: docker
commands:
- "docker login git.libertytechforce.com -u $DOCKER_USERNAME -p $DOCKER_TOKEN"
- "docker build -t git.libertytechforce.com/raoul/http2smtp:nightly -t git.libertytechforce.com/raoul/http2smtp:${CI_COMMIT_SHA:0:8} -f Dockerfile.nightly ."
- "docker push git.libertytechforce.com/raoul/http2smtp:nightly"
- "docker push git.libertytechforce.com/raoul/http2smtp:${CI_COMMIT_SHA:0:8}"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
secrets:
- docker_username
- docker_token
when:
- branch: master
event: push
release-image:
image: docker
commands:
- "docker login git.libertytechforce.com -u $DOCKER_USERNAME -p $DOCKER_TOKEN"
- "docker build -t git.libertytechforce.com/raoul/http2smtp:latest -t git.libertytechforce.com/raoul/http2smtp:${CI_COMMIT_TAG##v} -f Dockerfile.release ."
- "docker push git.libertytechforce.com/raoul/http2smtp:latest"
- "docker push git.libertytechforce.com/raoul/http2smtp:${CI_COMMIT_TAG##v}"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
secrets:
- docker_username
- docker_token
when:
- branch: master
event: tag

10
Dockerfile.nightly 100644
View File

@ -0,0 +1,10 @@
FROM python:3.11-alpine
WORKDIR /app
ADD . /app
RUN apk add git
RUN pip install -e .
RUN pip install uvicorn[standard]
EXPOSE 8000
CMD ["uvicorn", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers", "http2smtp.app:app"]

View File

@ -0,0 +1,6 @@
FROM python:3.11-alpine
RUN pip install --extra-index-url https://git.libertytechforce.com/api/packages/raoul/pypi/simple/ http2smtp uvicorn[standard]
EXPOSE 8000
CMD ["uvicorn", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers", "http2smtp.app:app"]

9
LICENSE.txt 100644
View File

@ -0,0 +1,9 @@
MIT License
Copyright (c) 2023-present Raoul Snyman <raoul@snyman.info>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

20
README.rst 100644
View File

@ -0,0 +1,20 @@
=========
http2smtp
=========
**Table of Contents**
- `Installation <#installation>`_
- `License <#license>`_
Installation
------------
.. code-block:: sh
pip install http2smtp
License
-------
``http2smtp`` is distributed under the terms of the `MIT <https://spdx.org/licenses/MIT.html>`_ license.

View File

@ -0,0 +1,4 @@
# SPDX-FileCopyrightText: 2023-present Raoul Snyman <raoul@snyman.info>
#
# SPDX-License-Identifier: MIT
__version__ = '0.0.1'

View File

@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2023-present Raoul Snyman <raoul@snyman.info>
#
# SPDX-License-Identifier: MIT

26
http2smtp/app.py 100644
View File

@ -0,0 +1,26 @@
from fastapi import FastAPI
from fastapi.responses import JSONResponse, RedirectResponse
from http2smtp.models import Email
from http2smtp.message import BodyRequiredError, build_message
from http2smtp.smtp import send_email
app = FastAPI()
@app.get('/')
async def index() -> RedirectResponse:
return RedirectResponse(url='/docs')
@app.post('/smtp')
async def smtp_send(email: Email):
try:
message = build_message(email)
except BodyRequiredError as e:
return JSONResponse(status=400, content={'error': str(e)})
try:
send_email(email.from_email, email.password, message)
except Exception as e:
return JSONResponse(status=400, content={'error': str(e)})
return {"status": "success"}

View File

@ -0,0 +1,38 @@
from bs4 import BeautifulSoup
from fastapi_mail import MessageSchema, MessageType
from http2smtp.models import Email
class BodyRequiredError(Exception):
def __init__(self):
super().__init__('Either "html_body" or "text_body" are required, but neither were supplied')
def _strip_html(html: str) -> str:
"""Strip HTML tags from the text and return just the plain text"""
soup = BeautifulSoup(html, 'html.parser')
return soup.get_text()
def build_message(email: Email) -> MessageSchema:
"""Build an EmailMessage from an Email"""
recipients: list[str] = [email.to_email] if isinstance(email.to_email, str) else email.to_email
msg = MessageSchema(
subject=email.subject,
recipients=recipients,
subtype=MessageType.html
)
if not email.html_body and not email.text_body:
raise BodyRequiredError()
if email.html_body and not email.text_body:
email.text_body = _strip_html(email.html_body)
if email.html_body:
msg.subtype = MessageType.html
msg.body = email.html_body
msg.alternative_body = email.text_body
else:
msg.subtype = MessageType.plain
msg.body = email.text_body
return msg

View File

@ -0,0 +1,10 @@
from pydantic import BaseModel
class Email(BaseModel):
from_email: str
password: str
to_email: str | list[str]
subject: str
html_body: str | None = None
text_body: str | None = None

View File

@ -0,0 +1,19 @@
from pydantic_settings import BaseSettings, SettingsConfigDict
from fastapi_mail import ConnectionConfig as BaseConfig
class Settings(BaseSettings):
host: str = 'localhost'
port: int = 8025
sslmode: str = 'none'
model_config = SettingsConfigDict(env_file='.env')
class MailConfig(BaseConfig):
model_config = SettingsConfigDict(env_file='.env')
settings = Settings()
mail_config = MailConfig(MAIL_USERNAME='', MAIL_PASSWORD='', MAIL_FROM='default@example.com', MAIL_PORT=25,
MAIL_SERVER='', MAIL_STARTTLS=False, MAIL_SSL_TLS=False)

12
http2smtp/smtp.py 100644
View File

@ -0,0 +1,12 @@
from fastapi_mail import FastMail, MessageSchema
from http2smtp.settings import mail_config
def send_email(from_address: str, password: str, message: MessageSchema) -> None:
"""Send the e-mail"""
mail_config.MAIL_USERNAME = from_address
mail_config.MAIL_PASSWORD = password
mail_config.MAIL_FROM = from_address
fm = FastMail(mail_config)
fm.send_message(message)

66
pyproject.toml 100644
View File

@ -0,0 +1,66 @@
[build-system]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"
[project]
name = "http2smtp"
description = 'An HTTP to SMTP gateway'
readme = "README.md"
requires-python = ">=3.11"
license = "MIT"
keywords = []
authors = [
{ name = "Raoul Snyman", email = "raoul@snyman.info" },
]
classifiers = [
"Development Status :: 4 - Beta",
"Programming Language :: Python",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
]
dependencies = [
"bs4",
"FastAPI",
"fastapi-mail",
"pydantic-settings",
"uvicorn[standard]"
]
dynamic = ["version"]
[project.urls]
Documentation = "https://git.libertytechforce.com/libertytechforce/http2smtp#readme"
Issues = "https://git.libertytechforce.com/libertytechforce/http2smtp/issues"
Source = "https://git.libertytechforce.com/libertytechforce/http2smtp"
[tool.hatch.version]
source = "vcs"
[tool.hatch.envs.default]
dependencies = [
"flake8",
"pytest",
"pytest-cov"
]
[tool.hatch.envs.default.scripts]
serve = "uvicorn http2smtp.app:app --reload"
lint = "flake8"
test = "pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=http2smtp --cov=tests {args}"
[[tool.hatch.envs.test.matrix]]
python = ["311"]
[tool.coverage.run]
branch = true
parallel = true
omit = [
"http2smtp/__about__.py",
]
[tool.coverage.report]
exclude_lines = [
"no cov",
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
]

View File

@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2023-present Raoul Snyman <raoul@snyman.info>
#
# SPDX-License-Identifier: MIT

View File

View File

@ -0,0 +1,30 @@
from http2smtp.models import Email
from http2smtp.message import build_message
def test_build_message_html():
"""Test building a message with an HTML body"""
subject = 'This is a test e-mail'
html_body = '<html><body><p>This is the body of the <strong>html</strong> e-mail.</p></body></html>'
email = Email(from_email='test@example.com', to_email='another@example.com', subject=subject,
html_body=html_body, password='dfsghasdf')
message = build_message(email)
assert message.subject == subject
assert message.body == html_body
assert message.alternative_body == 'This is the body of the html e-mail.'
def test_build_message_text():
"""Test building a message with a plain text body"""
subject = 'This is a test e-mail'
text_body = 'This is the body of the text e-mail.'
email = Email(from_email='test@example.com', to_email='another@example.com', subject=subject,
text_body=text_body, password='fghfhergsdf')
message = build_message(email)
assert message.subject == subject
assert message.body == text_body
assert message.alternative_body is None

24
tests/test_smtp.py 100644
View File

@ -0,0 +1,24 @@
import secrets
from fastapi_mail import MessageSchema, MessageType
from http2smtp.smtp import send_email
def test_send_email():
"""Test that we can send e-mail without SSL/TLS"""
# GIVEN: A valid login to the server
from_address = 'test@example.com'
password = secrets.token_urlsafe(8)
message = MessageSchema(
subject='Test e-mail',
recipients=['another@example.com'],
body='This is the body',
subtype=MessageType.plain
)
# WHEN: An e-mail is sent
send_email(from_address, password, message)
# THEN: The message should have been sent