Inception
This commit is contained in:
commit
a58a21f1b9
19 changed files with 12005 additions and 0 deletions
430
.gitignore
vendored
Normal file
430
.gitignore
vendored
Normal 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
37
mailhug-ui/package.json
Normal 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
43
mailhug-ui/src/App.css
Normal 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
59
mailhug-ui/src/App.js
Normal 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;
|
9
mailhug-ui/src/App.test.js
Normal file
9
mailhug-ui/src/App.test.js
Normal 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
13
mailhug-ui/src/index.css
Normal 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
12
mailhug-ui/src/index.js
Normal 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
7
mailhug-ui/src/logo.svg
Normal 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 |
137
mailhug-ui/src/serviceWorker.js
Normal file
137
mailhug-ui/src/serviceWorker.js
Normal 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();
|
||||
});
|
||||
}
|
||||
}
|
5
mailhug-ui/src/setupTests.js
Normal file
5
mailhug-ui/src/setupTests.js
Normal 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
10916
mailhug-ui/yarn.lock
Normal file
File diff suppressed because it is too large
Load diff
19
mailhug/Cargo.toml
Normal file
19
mailhug/Cargo.toml
Normal 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
21
mailhug/src/api.rs
Normal 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())
|
||||
}
|
42
mailhug/src/maildir/mem.rs
Normal file
42
mailhug/src/maildir/mem.rs
Normal 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});
|
||||
}
|
||||
}
|
||||
}
|
86
mailhug/src/maildir/mod.rs
Normal file
86
mailhug/src/maildir/mod.rs
Normal 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")
|
||||
}
|
||||
}
|
60
mailhug/src/maildir/tempfile.rs
Normal file
60
mailhug/src/maildir/tempfile.rs
Normal 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
71
mailhug/src/main.rs
Normal 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
22
mailhug/src/smtp.rs
Normal 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
16
mailhug/src/ui.rs
Normal 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))
|
||||
}
|
Loading…
Reference in a new issue