Inception

This commit is contained in:
Armin Friedl 2019-12-15 18:38:37 +01:00
commit a58a21f1b9
Signed by: armin
GPG key ID: 48C726EEE7FBCBC8
19 changed files with 12005 additions and 0 deletions

430
.gitignore vendored Normal file
View file

@ -0,0 +1,430 @@
# Created by https://www.gitignore.io/api/vim,node,rust,react,linux,macos,emacs,windows,eclipse,intellij+all,visualstudiocode
# Edit at https://www.gitignore.io/?templates=vim,node,rust,react,linux,macos,emacs,windows,eclipse,intellij+all,visualstudiocode
### Eclipse ###
.metadata
bin/
tmp/
*.tmp
*.bak
*.swp
*~.nib
local.properties
.settings/
.loadpath
.recommenders
# External tool builders
.externalToolBuilders/
# Locally stored "Eclipse launch configurations"
*.launch
# PyDev specific (Python IDE for Eclipse)
*.pydevproject
# CDT-specific (C/C++ Development Tooling)
.cproject
# CDT- autotools
.autotools
# Java annotation processor (APT)
.factorypath
# PDT-specific (PHP Development Tools)
.buildpath
# sbteclipse plugin
.target
# Tern plugin
.tern-project
# TeXlipse plugin
.texlipse
# STS (Spring Tool Suite)
.springBeans
# Code Recommenders
.recommenders/
# Annotation Processing
.apt_generated/
# Scala IDE specific (Scala & Java development for Eclipse)
.cache-main
.scala_dependencies
.worksheet
### Eclipse Patch ###
# Eclipse Core
.project
# JDT-specific (Eclipse Java Development Tools)
.classpath
# Annotation Processing
.apt_generated
.sts4-cache/
### Emacs ###
# -*- mode: gitignore; -*-
*~
\#*\#
/.emacs.desktop
/.emacs.desktop.lock
*.elc
auto-save-list
tramp
.\#*
# Org-mode
.org-id-locations
*_archive
# flymake-mode
*_flymake.*
# eshell files
/eshell/history
/eshell/lastdir
# elpa packages
/elpa/
# reftex files
*.rel
# AUCTeX auto folder
/auto/
# cask packages
.cask/
dist/
# Flycheck
flycheck_*.el
# server auth directory
/server/
# projectiles files
.projectile
# directory configuration
.dir-locals.el
# network security
/network-security.data
### Intellij+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### Intellij+all Patch ###
# Ignores the whole .idea folder and all .iml files
# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
.idea/
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
*.iml
modules.xml
.idea/misc.xml
*.ipr
# Sonarlint plugin
.idea/sonarlint
### Linux ###
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# react / gatsby
public/
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
### react ###
.DS_*
**/*.backup.*
**/*.back.*
node_modules
bower_componets
*.sublime*
psd
thumb
sketch
build/
### Rust ###
# Generated by Cargo
# will have compiled files and executables
target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
### Vim ###
# Swap
[._]*.s[a-v][a-z]
[._]*.sw[a-p]
[._]s[a-rt-v][a-z]
[._]ss[a-gi-z]
[._]sw[a-p]
# Session
Session.vim
Sessionx.vim
# Temporary
.netrwhist
# Auto-generated tag files
tags
# Persistent undo
[._]*.un~
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
### Windows ###
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# End of https://www.gitignore.io/api/vim,node,rust,react,linux,macos,emacs,windows,eclipse,intellij+all,visualstudiocode

37
mailhug-ui/package.json Normal file
View file

@ -0,0 +1,37 @@
{
"name": "mailhug-ui",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"dotenv": "^8.2.0",
"grommet": "^2.8.1",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-scripts": "3.3.0",
"styled-components": "^4.4.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

43
mailhug-ui/src/App.css Normal file
View file

@ -0,0 +1,43 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
iframe {
border: none;
width: 100%;
}

59
mailhug-ui/src/App.js Normal file
View file

@ -0,0 +1,59 @@
import { Box, Grommet, Table, TableHeader, TableRow, TableCell, TableBody, Collapsible } from 'grommet';
import { grommet } from "grommet/themes";
import React, { Component } from 'react';
import './App.css';
const host = (url) => url;
const MailhugTableHeader = () => {return(
<TableHeader>
<TableRow>
<TableCell scope="col" border="bottom"> Subject </TableCell>
<TableCell scope="col" border="bottom"> Recipient </TableCell>
<TableCell scope="col" border="bottom"> Date </TableCell>
</TableRow>
</TableHeader>
);};
class App extends Component {
constructor(props) {
super(props);
this.state = {mailtable: [], mailurl: "/mail/99"};
}
componentDidMount() {
fetch("/mails")
.then(mt => mt.json())
.then(data => this.setState({mailtable: data}));
}
resizeIframe(obj){
obj.style.height = 0;
obj.style.height = obj.contentWindow.document.body.scrollHeight + 'px';
}
render() {
return (
<Grommet theme={grommet} height={{min: "100vh"}} fill>
<Box direction="row" pad="small" height={{ max: "small"}} overflow="auto" border fill>
<Table>
<MailhugTableHeader/>
<TableBody>
{this.state.mailtable.map((mail) => {
return (<TableRow onClick={() => this.setState({mailurl: host(`mail/${mail.id}`)})} hoverIndicator="background">
<TableCell scope="row"> <strong>{mail.subject}</strong> </TableCell>
<TableCell>{mail.from}</TableCell>
</TableRow>);
})}
</TableBody>
</Table>
</Box>
<Box direction="row" pad="medium" height={{min: "large"}} fill>
<iframe title="mail" src={this.state.mailurl} />
</Box>
</Grommet>
);
}
}
export default App;

View file

@ -0,0 +1,9 @@
import React from 'react';
import { render } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
const { getByText } = render(<App />);
const linkElement = getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

13
mailhug-ui/src/index.css Normal file
View file

@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

12
mailhug-ui/src/index.js Normal file
View file

@ -0,0 +1,12 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
ReactDOM.render(<App />, document.getElementById('root'));
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

7
mailhug-ui/src/logo.svg Normal file
View file

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
<g fill="#61DAFB">
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
<circle cx="420.9" cy="296.5" r="45.7"/>
<path d="M520.5 78.1z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -0,0 +1,137 @@
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://bit.ly/CRA-PWA
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.0/8 are considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export function register(config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://bit.ly/CRA-PWA'
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl, config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl, {
headers: { 'Service-Worker': 'script' }
})
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}

View file

@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom/extend-expect';

10916
mailhug-ui/yarn.lock Normal file

File diff suppressed because it is too large Load diff

19
mailhug/Cargo.toml Normal file
View file

@ -0,0 +1,19 @@
[package]
name = "mailhug"
version = "0.1.0"
authors = ["Armin Friedl <dev@friedl.net>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rocket = "0.4.2"
rocket_contrib = "0.4.2"
serde = {version = "1.0", features = ["derive"]}
mailin-embedded = "0.5"
log = "0.4"
env_logger = "0.7"
structopt = "0.3"
quick-error = "1.2"
mailparse = "0.1"
rand = "0.7"

21
mailhug/src/api.rs Normal file
View file

@ -0,0 +1,21 @@
#[allow(unused_imports)]
use log::{debug, error, info, trace, warn};
use rocket::get;
use rocket::http::ContentType;
use rocket::response::content::Content;
use rocket::State;
use rocket_contrib::json::Json;
use crate::maildir::MailInfo;
use crate::maildir::Maildir;
#[get("/mail/<id>")]
pub fn mail(id: String, maildir: State<Maildir>) -> Content<String> {
Content(ContentType::HTML, maildir.mail_content(&id))
}
#[get("/mails")]
pub fn mailtable(maildir: State<Maildir>) -> Json<Vec<MailInfo>> {
Json(maildir.mail_infos())
}

View file

@ -0,0 +1,42 @@
use std::io::{Read,Write};
use std::fs;
use std::path::Path;
use serde::{Serialize};
use mailparse::{parse_headers, MailHeaderMap};
#[derive(Default)]
pub struct MailMem {
pub mailinfos: Vec<MailInfo>
}
#[derive(Serialize, Clone)]
pub struct MailInfo {
id: String,
from: String,
to: String,
subject: String,
}
impl MailMem {
pub fn init(&mut self, cur_dir: &Path) {
for entry in fs::read_dir(cur_dir).unwrap() {
let entry: fs::DirEntry = entry.unwrap();
let metadata: fs::Metadata = entry.metadata().unwrap();
assert!(metadata.is_file());
let mut file = fs::File::open(entry.path()).unwrap();
let mut raw_mail = Vec::new();
file.read_to_end(&mut raw_mail).unwrap();
let (headers, _) = parse_headers(&raw_mail).unwrap();
let id = entry.file_name().into_string().unwrap();
let from = headers.get_first_value("From").unwrap().unwrap();
let to = headers.get_first_value("To").unwrap().unwrap();
let subject = headers.get_first_value("Subject").unwrap().unwrap();
self.mailinfos.push(MailInfo {id, from, to, subject});
}
}
}

View file

@ -0,0 +1,86 @@
#[allow(unused_imports)]
use log::{error, warn, info, debug, trace};
use std::path::PathBuf;
use std::io::Read;
mod mem;
mod tempfile;
pub use tempfile::TempFile;
pub use mem::MailInfo;
#[derive(Clone)]
pub struct Maildir (std::sync::Arc<std::sync::RwLock<MaildirInternal>>);
impl Maildir {
pub fn mail_infos(&self) -> Vec<MailInfo> {
self.0.read().unwrap().mem.mailinfos.to_vec()
}
pub fn mail_content(&self, id: &str) -> String {
let cur_dir = self.0.read().unwrap().cur_dir();
let new_dir = self.0.read().unwrap().new_dir();
let mut mail_file = match std::fs::File::open(cur_dir.join(id)) {
Ok(f) => f,
_ => std::fs::File::open(new_dir.join(id)).unwrap()
};
let mut mail_buf = Vec::new();
mail_file.read_to_end(&mut mail_buf).unwrap();
let mail: mailparse::ParsedMail = mailparse::parse_mail(&mail_buf).unwrap();
mail.get_body().unwrap()
}
pub fn tmp_file(&self) -> TempFile {
tempfile::TempFile::new(self)
}
}
impl From<PathBuf> for Maildir {
fn from(path: PathBuf) -> Self {
let mut md_internal = MaildirInternal{
path: path.clone(),
mem: mem::MailMem::default()};
if !path.exists() {
std::fs::create_dir_all(&path).unwrap();
}
if !md_internal.cur_dir().exists() {
std::fs::create_dir_all(&md_internal.cur_dir()).unwrap();
}
if !md_internal.new_dir().exists() {
std::fs::create_dir_all(&md_internal.new_dir()).unwrap();
}
if !md_internal.tmp_dir().exists() {
std::fs::create_dir_all(&md_internal.tmp_dir()).unwrap();
}
md_internal.mem.init(&md_internal.new_dir());
Maildir(std::sync::Arc::new(std::sync::RwLock::new(md_internal)))
}
}
struct MaildirInternal {
path: PathBuf,
mem: mem::MailMem
}
impl MaildirInternal {
fn cur_dir(&self) -> PathBuf {
self.path.join("cur")
}
fn tmp_dir(&self) -> PathBuf {
self.path.join("tmp")
}
fn new_dir(&self) -> PathBuf {
self.path.join("new")
}
}

View file

@ -0,0 +1,60 @@
#[allow(unused_imports)]
use log::{error, warn, info, debug, trace};
use std::io::{Read, Write};
use super::Maildir;
pub struct TempFile {
buffer: Vec<u8>,
maildir: Maildir
}
impl TempFile {
pub fn new(maildir: &Maildir) -> Self {
TempFile {
buffer: Vec::new(),
maildir: maildir.clone()
}
}
}
impl Write for TempFile {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.buffer.write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
self.buffer.flush()
}
}
impl Read for TempFile {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.buffer.as_slice().read(buf)
}
}
impl Drop for TempFile {
fn drop(&mut self) {
let eid = std::time::SystemTime::now()
.duration_since(std::time::SystemTime::UNIX_EPOCH)
.expect("Could not get current time stamp")
.as_nanos();
let pid = std::process::id();
let rid = rand::random::<u32>();
let tmp_name = format!{"{:x}{:x}{:x}", eid, pid, rid};
let tmp_file_path = self.maildir.0.read().unwrap()
.tmp_dir().join(&tmp_name);
let new_file_path = self.maildir.0.read().unwrap()
.new_dir().join(&tmp_name);
debug!{"Writing into tmp file {}", tmp_file_path.display()}
let mut tmp_file = std::fs::File::create(&tmp_file_path).unwrap();
tmp_file.write(&self.buffer).unwrap();
std::fs::rename(tmp_file_path, new_file_path).unwrap();
}
}

71
mailhug/src/main.rs Normal file
View file

@ -0,0 +1,71 @@
#![feature(proc_macro_hygiene, decl_macro)]
#[allow(unused_imports)]
use log::{error, warn, info, debug, trace};
use rocket::fairing::AdHoc;
use rocket::http::hyper::header::AccessControlAllowOrigin;
use rocket::routes;
use structopt::StructOpt;
use std::path::PathBuf;
use mailin_embedded::{Server, SslConfig};
mod api;
mod ui;
mod smtp;
mod maildir;
use smtp::MailinHandler;
#[derive(StructOpt)]
struct Config {
#[structopt(env="MAILDIR", parse(from_os_str), default_value="./maildir")]
maildir: PathBuf,
#[structopt(env="HOST_NAME", default_value="example.com")]
host_name: String,
#[structopt(env="HOST_IP", default_value="127.0.0.1")]
host_ip: String,
#[structopt(env="HTTP_PORT", default_value="8000")]
ui_port: u16,
#[structopt(env="SMTP_PORT", default_value="9025")]
smtp_port: u16,
}
fn main() {
env_logger::init();
let config = Config::from_args();
info!{"Initializing Maildir"}
let maildir = maildir::Maildir::from(config.maildir);
info!{"Spawning SMTP sink"}
let mailin_handler = MailinHandler::new(maildir.clone());
let mut server = Server::new(mailin_handler);
server.with_name(config.host_name)
.with_ssl(SslConfig::None)
.expect("Could not configure SMTP sink without SSL")
.with_addr((config.host_ip.as_ref(), config.smtp_port))
.expect(&format!{"Could set SMTP sink address to {}:{}",
config.host_ip, config.smtp_port});
server.serve().expect("Could not start SMTP sink");
info!{"Spawning HTTP services"}
rocket::ignite()
.attach(AdHoc::on_response("CORS", |_req, resp|
{ resp.set_header(AccessControlAllowOrigin::Any);}))
.manage(maildir)
.mount("/", routes![ui::index,
ui::statics,
api::mailtable,
api::mail])
.launch();
}

22
mailhug/src/smtp.rs Normal file
View file

@ -0,0 +1,22 @@
#[allow(unused_imports)]
use log::{error, warn, info, debug, trace};
use mailin_embedded::{Handler, DataResult};
use crate::maildir::Maildir;
#[derive(Clone)]
pub struct MailinHandler {
maildir: Maildir
}
impl MailinHandler {
pub fn new(maildir: Maildir) -> Self {
MailinHandler{maildir}
}
}
impl Handler for MailinHandler {
fn data(&mut self, _domain: &str, _from: &str, _is8bit: bool, _to: &[String]) -> DataResult {
DataResult::Ok(Box::new(self.maildir.tmp_file()))
}
}

16
mailhug/src/ui.rs Normal file
View file

@ -0,0 +1,16 @@
#[allow(unused_imports)]
use log::{error, warn, info, debug, trace};
use std::path::{Path,PathBuf};
use rocket::get;
use rocket::response::NamedFile;
#[get("/")]
pub fn index() -> Result<NamedFile, std::io::Error> {
NamedFile::open("../mailhug-ui/build/index.html")
}
#[get("/static/<path..>")]
pub fn statics(path: PathBuf) -> Result<NamedFile, std::io::Error> {
NamedFile::open(Path::new("../mailhug-ui/build/static/").join(path))
}