Compare commits
8 Commits
Author | SHA1 | Date |
---|---|---|
Jacob Kiers | 6859de9f7d | |
Jacob Kiers | 007f9f9112 | |
Jacob Kiers | 7d75389a09 | |
Jacob Kiers | 054324cdd7 | |
Jacob Kiers | 47e0bd9012 | |
Jacob Kiers | 0569591aa5 | |
Jacob Kiers | ac51541c0c | |
Jacob Kiers | a3317592e1 |
|
@ -1,5 +1,5 @@
|
||||||
local executableName = 'newsletter-to-web';
|
local executableName = 'newsletter-to-web';
|
||||||
local cross_image = 'img.kie.rs/jjkiers/rust-dind-cross:1.66-full';
|
local build_image = 'img.kie.rs/jjkiers/rust-cross:rust1.70-zig';
|
||||||
|
|
||||||
local archs = [
|
local archs = [
|
||||||
{ target: 'aarch64-unknown-linux-musl', short: 'arm64-musl' },
|
{ target: 'aarch64-unknown-linux-musl', short: 'arm64-musl' },
|
||||||
|
@ -12,26 +12,30 @@ local getStepName(arch) = 'Build for ' + arch.short;
|
||||||
local builtExecutableName(arch) = executableName + if std.length(std.findSubstr(arch.short, 'windows')) > 0 then '.exe' else '';
|
local builtExecutableName(arch) = executableName + if std.length(std.findSubstr(arch.short, 'windows')) > 0 then '.exe' else '';
|
||||||
local targetExecutableName(arch) = executableName + '-' + arch.target + if std.length(std.findSubstr(arch.short, 'windows')) > 0 then '.exe' else '';
|
local targetExecutableName(arch) = executableName + '-' + arch.target + if std.length(std.findSubstr(arch.short, 'windows')) > 0 then '.exe' else '';
|
||||||
|
|
||||||
|
local getVolumeName(arch) = 'target-' + arch.target;
|
||||||
|
local getLocalVolumes(arch) = [
|
||||||
|
{
|
||||||
|
name: getVolumeName(arch),
|
||||||
|
temp: {},
|
||||||
|
}
|
||||||
|
for arch in archs
|
||||||
|
];
|
||||||
|
|
||||||
local add_build_steps() = [
|
local add_build_steps() = [
|
||||||
{
|
{
|
||||||
name: getStepName(arch),
|
name: getStepName(arch),
|
||||||
image: cross_image,
|
image: build_image,
|
||||||
volumes: [
|
|
||||||
{
|
|
||||||
name: 'dockersock',
|
|
||||||
path: '/var/run',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
commands: [
|
commands: [
|
||||||
'echo Hello World from Jsonnet on ' + arch.target + '!',
|
'echo Hello World from Jsonnet on ' + arch.target + '!',
|
||||||
'cross build --release --target ' + arch.target,
|
'cargo zigbuild --release --target ' + arch.target,
|
||||||
'cp target/' + arch.target + '/release/' + builtExecutableName(arch) + ' artifacts/' + targetExecutableName(arch),
|
'cp target/' + arch.target + '/release/' + builtExecutableName(arch) + ' artifacts/' + targetExecutableName(arch),
|
||||||
'rm -rf target/' + arch.target + '/release/*',
|
'rm -rf target/' + arch.target + '/release/*',
|
||||||
],
|
],
|
||||||
environment: {
|
depends_on: ['Prepare'],
|
||||||
CROSS_REMOTE: true,
|
volumes: [{
|
||||||
},
|
name: getVolumeName(arch),
|
||||||
depends_on: ['Wait for Docker'],
|
path: '/drone/src/target',
|
||||||
|
}],
|
||||||
}
|
}
|
||||||
for arch in archs
|
for arch in archs
|
||||||
];
|
];
|
||||||
|
@ -45,30 +49,20 @@ local add_build_steps() = [
|
||||||
},
|
},
|
||||||
steps:
|
steps:
|
||||||
[{
|
[{
|
||||||
name: 'Wait for Docker',
|
name: 'Prepare',
|
||||||
image: cross_image,
|
image: build_image,
|
||||||
commands: [
|
commands: [
|
||||||
'mkdir artifacts',
|
'mkdir artifacts',
|
||||||
'echo Using image: ' + cross_image,
|
'echo Using image: ' + build_image,
|
||||||
'while ! docker image ls; do sleep 1; done',
|
|
||||||
'cargo --version',
|
'cargo --version',
|
||||||
'rustc --version',
|
'rustc --version',
|
||||||
'docker info',
|
|
||||||
'docker pull hello-world:latest',
|
|
||||||
],
|
],
|
||||||
environment: {
|
|
||||||
CROSS_REMOTE: true,
|
|
||||||
},
|
|
||||||
volumes: [{
|
|
||||||
name: 'dockersock',
|
|
||||||
path: '/var/run',
|
|
||||||
}],
|
|
||||||
}] +
|
}] +
|
||||||
add_build_steps() +
|
add_build_steps() +
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
name: 'Show built artifacts',
|
name: 'Show built artifacts',
|
||||||
image: cross_image,
|
image: build_image,
|
||||||
commands: [
|
commands: [
|
||||||
'ls -lah artifacts',
|
'ls -lah artifacts',
|
||||||
],
|
],
|
||||||
|
@ -92,28 +86,7 @@ local add_build_steps() = [
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
services: [{
|
volumes: getLocalVolumes(archs),
|
||||||
name: 'docker',
|
|
||||||
image: 'docker:dind',
|
|
||||||
privileged: true,
|
|
||||||
volumes: [
|
|
||||||
{
|
|
||||||
name: 'dockersock',
|
|
||||||
path: '/var/run',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'docker-storage',
|
|
||||||
path: '/var/lib/docker',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}],
|
|
||||||
|
|
||||||
volumes: [
|
|
||||||
{
|
|
||||||
name: 'dockersock',
|
|
||||||
temp: {},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
image_pull_secrets: ['docker_private_repo'],
|
image_pull_secrets: ['docker_private_repo'],
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
* Added protection features to example systemd service file
|
||||||
|
|
||||||
## [0.2.3] - 2022-12-29
|
## [0.2.3] - 2022-12-29
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
22
Cargo.toml
22
Cargo.toml
|
@ -11,15 +11,13 @@ authors = [
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
atom_syndication = "^0.11.0"
|
atom_syndication = "0.12.0"
|
||||||
base16ct = { version = "^0.1.0", features = [ "alloc" ] }
|
base16ct = { version = "0.2.0", features = [ "alloc" ] }
|
||||||
chrono = "^0.4"
|
chrono = "0.4.23"
|
||||||
clap = { version = "^4.0.22", features = [ "derive" ] }
|
clap = { version = "4.2.7", features = [ "derive" ] }
|
||||||
imap = { version = "^2.4.1", default-features = false }
|
imap = { version = "3.0.0-alpha.10", default-features = false }
|
||||||
mail-parser = "^0.8.0"
|
mail-parser = "0.8.2"
|
||||||
rustls-connector = { version = "^0.16.1", default-features = false, features = [ "webpki-roots-certs", "quic" ] }
|
rustls-connector = { version = "0.17.0", default-features = false, features = [ "webpki-roots-certs" ] }
|
||||||
sha2 = "^0.10.2"
|
self_update = { version = "0.36.0", default-features = false, features = ["rustls"] }
|
||||||
self_update = { version = "0.33.0", default-features = false, features = ["rustls"] }
|
sha2 = "0.10.6"
|
||||||
|
reqwest = { version = "0.11.14", default-features = false, features = ["rustls", "blocking"] }
|
||||||
[patch.crates-io]
|
|
||||||
atom_syndication = { git = "https://github.com/rust-syndication/atom", rev = "5cf8d161e5e5af7d93cca8d2c117b7af879a99b7" }
|
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -x
|
||||||
|
apt-get update && apt-get install -y jq curl
|
||||||
|
|
||||||
|
pushd $(mktemp -d)
|
||||||
|
|
||||||
|
# Install minisign
|
||||||
|
wget -nv https://github.com/jedisct1/minisign/releases/download/0.11/minisign-0.11-linux.tar.gz -O - | tar --strip-components 2 -C /usr/local/bin/ -vxzf - minisign-linux/x86_64/minisign
|
||||||
|
|
||||||
|
# Installing zig, checking its validity
|
||||||
|
mkdir -p /usr/local/bin
|
||||||
|
wget -nv https://ziglang.org/download/index.json -O zig-versions.json
|
||||||
|
|
||||||
|
echo "$(jq -r '.master."x86_64-linux".shasum' zig-versions.json) zig.tar.xz" > zig.tar.xz.shasum
|
||||||
|
wget -nv -c $(jq -r '.master."x86_64-linux".tarball' zig-versions.json) -O zig.tar.xz
|
||||||
|
wget -nv -c "$(jq -r '.master."x86_64-linux".tarball' zig-versions.json).minisig" -O zig.tar.xz.minisig
|
||||||
|
sha256sum -c zig.tar.xz.shasum
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Error in SHA256 hash!"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "SHA256 hash OK"
|
||||||
|
fi
|
||||||
|
|
||||||
|
minisign -P 'RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U' -Vm zig.tar.xz
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Error in signature!"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "Signature OK"
|
||||||
|
fi
|
||||||
|
|
||||||
|
tar -C /usr/local/bin --strip-components 1 -xf zig.tar.xz
|
||||||
|
|
||||||
|
rm -rf zig*
|
||||||
|
|
||||||
|
# Installing binstall and zigbuild
|
||||||
|
curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash
|
||||||
|
cargo binstall -y cargo-zigbuild
|
||||||
|
|
||||||
|
popd
|
48
src/cli.rs
48
src/cli.rs
|
@ -1,6 +1,6 @@
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Args, Parser, Subcommand};
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[clap(author, version, about, long_about = None)]
|
#[clap(author, version, about, long_about = None)]
|
||||||
|
@ -9,35 +9,41 @@ pub(crate) struct Cli {
|
||||||
pub command: Command,
|
pub command: Command,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub(crate) struct ImapSettings {
|
||||||
|
#[clap(short, long, value_parser)]
|
||||||
|
pub server: String,
|
||||||
|
#[clap(long, value_parser, default_value_t = 993)]
|
||||||
|
pub port: u16,
|
||||||
|
#[clap(short, long, value_parser)]
|
||||||
|
pub username: String,
|
||||||
|
#[clap(short, long, value_parser)]
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub(crate) struct FeedBuilderSettings {
|
||||||
|
/// Host name hosting the feed
|
||||||
|
pub hostname: String,
|
||||||
|
/// Feed file
|
||||||
|
#[clap(value_parser, default_value = "output/feed.xml")]
|
||||||
|
pub filename: PathBuf,
|
||||||
|
/// Create an HTML file for each message
|
||||||
|
#[clap(short, long, value_parser, default_value_t = false)]
|
||||||
|
pub include_html: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
pub(crate) enum Command {
|
pub(crate) enum Command {
|
||||||
/// Fetch emails from an IMAP server
|
/// Fetch emails from an IMAP server
|
||||||
FetchFromImap {
|
FetchFromImap(ImapSettings),
|
||||||
#[clap(short, long, value_parser)]
|
|
||||||
server: String,
|
|
||||||
#[clap(long, value_parser, default_value_t = 993)]
|
|
||||||
port: u16,
|
|
||||||
#[clap(short, long, value_parser)]
|
|
||||||
username: String,
|
|
||||||
#[clap(short, long, value_parser)]
|
|
||||||
password: String,
|
|
||||||
},
|
|
||||||
/// Fetch an email from a .eml file
|
/// Fetch an email from a .eml file
|
||||||
FetchFromFile {
|
FetchFromFile {
|
||||||
#[clap(value_parser)]
|
#[clap(value_parser)]
|
||||||
filename: PathBuf,
|
filename: PathBuf,
|
||||||
},
|
},
|
||||||
/// Build an ATOM feed containing the full contents of the email
|
/// Build an ATOM feed containing the full contents of the email
|
||||||
BuildFeed {
|
BuildFeed(FeedBuilderSettings),
|
||||||
/// Host name hosting the feed
|
|
||||||
hostname: String,
|
|
||||||
/// Feed file
|
|
||||||
#[clap(value_parser, default_value = "output/feed.xml")]
|
|
||||||
filename: PathBuf,
|
|
||||||
/// Create an HTML file for each message
|
|
||||||
#[clap(short, long, value_parser, default_value_t = false)]
|
|
||||||
include_html: bool,
|
|
||||||
},
|
|
||||||
/// Exports the emails as HTML files
|
/// Exports the emails as HTML files
|
||||||
ExportHtml {
|
ExportHtml {
|
||||||
/// The directory in which the emails will be stored
|
/// The directory in which the emails will be stored
|
||||||
|
|
10
src/feed.rs
10
src/feed.rs
|
@ -11,8 +11,8 @@ use mail_parser::HeaderValue;
|
||||||
pub(crate) fn add_entry_to_feed(
|
pub(crate) fn add_entry_to_feed(
|
||||||
feed: &mut Feed,
|
feed: &mut Feed,
|
||||||
message: &Message,
|
message: &Message,
|
||||||
processed_html: &String,
|
processed_html: &str,
|
||||||
hostname: &String,
|
hostname: &str,
|
||||||
include_html: bool,
|
include_html: bool,
|
||||||
) {
|
) {
|
||||||
let parsed = message.get_parsed().unwrap();
|
let parsed = message.get_parsed().unwrap();
|
||||||
|
@ -39,7 +39,7 @@ pub(crate) fn add_entry_to_feed(
|
||||||
uri: None,
|
uri: None,
|
||||||
},
|
},
|
||||||
title: parsed.subject().expect("Expected a subject").to_string(),
|
title: parsed.subject().expect("Expected a subject").to_string(),
|
||||||
content: Some(processed_html.clone()),
|
content: Some(processed_html.to_owned()),
|
||||||
id: url.clone(),
|
id: url.clone(),
|
||||||
published: Utc.timestamp_opt(date.to_timestamp(), 0).unwrap(),
|
published: Utc.timestamp_opt(date.to_timestamp(), 0).unwrap(),
|
||||||
url: match include_html {
|
url: match include_html {
|
||||||
|
@ -53,13 +53,13 @@ pub(crate) fn add_entry_to_feed(
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn build_atom_feed(hostname: &String, feed_file: &str) -> Feed {
|
pub(crate) fn build_atom_feed(hostname: &String, feed_file: &str) -> Feed {
|
||||||
let feed_url = format!("https://{}/{}", hostname, feed_file);
|
let feed_url = format!("https://{hostname}/{feed_file}");
|
||||||
FeedBuilder::default()
|
FeedBuilder::default()
|
||||||
.title("JJKiers Newsletters")
|
.title("JJKiers Newsletters")
|
||||||
.id(&feed_url)
|
.id(&feed_url)
|
||||||
.link(
|
.link(
|
||||||
LinkBuilder::default()
|
LinkBuilder::default()
|
||||||
.href(format!("https://{}/", hostname))
|
.href(format!("https://{hostname}/"))
|
||||||
.rel("alternate".to_string())
|
.rel("alternate".to_string())
|
||||||
.build(),
|
.build(),
|
||||||
)
|
)
|
||||||
|
|
47
src/main.rs
47
src/main.rs
|
@ -1,4 +1,4 @@
|
||||||
#[warn(missing_docs)]
|
#[deny(missing_docs)]
|
||||||
#[doc = include_str!("../README.md")]
|
#[doc = include_str!("../README.md")]
|
||||||
mod cli;
|
mod cli;
|
||||||
mod command;
|
mod command;
|
||||||
|
@ -29,24 +29,22 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||||
let data_directory = "data";
|
let data_directory = "data";
|
||||||
|
|
||||||
let result = match &cli.command {
|
let result = match &cli.command {
|
||||||
cli::Command::FetchFromImap {
|
cli::Command::FetchFromImap(imap_settings) => fetch_from_imap(
|
||||||
server,
|
|
||||||
port,
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
} => fetch_from_imap(
|
|
||||||
data_directory,
|
data_directory,
|
||||||
server.to_owned(),
|
imap_settings.server.to_owned(),
|
||||||
*port,
|
imap_settings.port,
|
||||||
username.to_owned(),
|
imap_settings.username.to_owned(),
|
||||||
password.to_owned(),
|
imap_settings.password.to_owned(),
|
||||||
),
|
),
|
||||||
cli::Command::BuildFeed {
|
|
||||||
filename,
|
cli::Command::BuildFeed(settings) => build_feed(
|
||||||
hostname,
|
&settings.filename,
|
||||||
include_html,
|
&settings.hostname,
|
||||||
} => build_feed(filename, hostname, *include_html),
|
settings.include_html,
|
||||||
|
),
|
||||||
|
|
||||||
cli::Command::Update => command::update::self_update(),
|
cli::Command::Update => command::update::self_update(),
|
||||||
|
|
||||||
_ => unimplemented!("This method is not yet implemented."),
|
_ => unimplemented!("This method is not yet implemented."),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -85,7 +83,7 @@ fn build_feed(
|
||||||
.to_str()
|
.to_str()
|
||||||
.expect("Feed path should be printable.");
|
.expect("Feed path should be printable.");
|
||||||
|
|
||||||
let mut feed = feed::build_atom_feed(&hostname, feed_file);
|
let mut feed = feed::build_atom_feed(hostname, feed_file);
|
||||||
|
|
||||||
let mut reader = DataDirectoryMessageReader::new(Path::new("data").to_path_buf());
|
let mut reader = DataDirectoryMessageReader::new(Path::new("data").to_path_buf());
|
||||||
|
|
||||||
|
@ -111,10 +109,10 @@ fn build_feed(
|
||||||
|
|
||||||
if include_html {
|
if include_html {
|
||||||
let path: PathBuf = [dir, Path::new(&get_path(&parsed, &msg))].iter().collect();
|
let path: PathBuf = [dir, Path::new(&get_path(&parsed, &msg))].iter().collect();
|
||||||
write_file(&path, processed_html.as_bytes())?;
|
write_file(path, processed_html.as_bytes())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
feed::add_entry_to_feed(&mut feed, &msg, &processed_html, &hostname, include_html);
|
feed::add_entry_to_feed(&mut feed, &msg, &processed_html, hostname, include_html);
|
||||||
}
|
}
|
||||||
|
|
||||||
if !feed.entries.is_empty() {
|
if !feed.entries.is_empty() {
|
||||||
|
@ -143,7 +141,7 @@ fn fetch_from_imap(
|
||||||
) -> Result<(), Box<dyn Error>> {
|
) -> Result<(), Box<dyn Error>> {
|
||||||
create_directory(data_directory)?;
|
create_directory(data_directory)?;
|
||||||
|
|
||||||
print!("Getting mail from {} for mailbox {}", server, username);
|
print!("Getting mail from {server} for mailbox {username}");
|
||||||
|
|
||||||
let mut reader = ImapReader::new(server, port, username, password);
|
let mut reader = ImapReader::new(server, port, username, password);
|
||||||
|
|
||||||
|
@ -168,12 +166,9 @@ fn fetch_from_imap(
|
||||||
);
|
);
|
||||||
|
|
||||||
let path = get_path(&parsed, &msg);
|
let path = get_path(&parsed, &msg);
|
||||||
let html_path: PathBuf = [
|
let html_path: PathBuf = [Path::new(data_directory), Path::new(&format!("{path}.eml"))]
|
||||||
Path::new(data_directory),
|
.iter()
|
||||||
Path::new(&format!("{}.eml", path)),
|
.collect();
|
||||||
]
|
|
||||||
.iter()
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
println!("Storing to {}", &html_path.display());
|
println!("Storing to {}", &html_path.display());
|
||||||
|
|
||||||
|
|
|
@ -1,165 +1,13 @@
|
||||||
use std::{
|
use std::vec::IntoIter;
|
||||||
collections::HashMap,
|
|
||||||
error::Error,
|
|
||||||
fs::{read_dir, DirEntry},
|
|
||||||
net::TcpStream,
|
|
||||||
path::PathBuf,
|
|
||||||
vec::IntoIter,
|
|
||||||
};
|
|
||||||
|
|
||||||
use imap::Session;
|
pub(crate) mod data_directory;
|
||||||
use rustls_connector::RustlsConnector;
|
pub(crate) mod imap;
|
||||||
|
|
||||||
|
pub(crate) use data_directory::DataDirectoryMessageReader;
|
||||||
|
pub(crate) use crate::message_reader::imap::ImapReader;
|
||||||
|
|
||||||
use crate::Message;
|
use crate::Message;
|
||||||
|
|
||||||
pub(crate) trait EmailReader {
|
pub(crate) trait EmailReader {
|
||||||
fn read_rfc822_messages(&mut self) -> Box<IntoIter<Message>>;
|
fn read_rfc822_messages(&mut self) -> Box<IntoIter<Message>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct DataDirectoryMessageReader {
|
|
||||||
path: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DataDirectoryMessageReader {
|
|
||||||
pub fn new(path: PathBuf) -> Self {
|
|
||||||
DataDirectoryMessageReader { path }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EmailReader for DataDirectoryMessageReader {
|
|
||||||
fn read_rfc822_messages(&mut self) -> Box<IntoIter<Message>> {
|
|
||||||
println!("Reading files in {}", &self.path.display());
|
|
||||||
let reader = match read_dir(&self.path) {
|
|
||||||
Ok(r) => r,
|
|
||||||
Err(e) => {
|
|
||||||
dbg!(e);
|
|
||||||
return Box::new(Vec::new().into_iter());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let items = reader
|
|
||||||
.filter(|i| i.is_ok())
|
|
||||||
.map(|i| i.unwrap() as DirEntry)
|
|
||||||
.filter(|d| match d.path().extension() {
|
|
||||||
Some(ext) => ext == "eml",
|
|
||||||
None => false,
|
|
||||||
})
|
|
||||||
.filter_map(|i| {
|
|
||||||
let uid = i
|
|
||||||
.path()
|
|
||||||
.file_stem()
|
|
||||||
.unwrap()
|
|
||||||
.to_owned()
|
|
||||||
.into_string()
|
|
||||||
.expect("Could not convert filename to string.")
|
|
||||||
.split('_')
|
|
||||||
.collect::<Vec<&str>>()[0]
|
|
||||||
.trim_start_matches('0')
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
if let Ok(data) = std::fs::read(i.path()) {
|
|
||||||
Some((uid, data))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.map(|i| Message::new(i.0, i.1));
|
|
||||||
|
|
||||||
let iter = items.collect::<Vec<Message>>().into_iter();
|
|
||||||
|
|
||||||
Box::new(iter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ImapReader {
|
|
||||||
host: String,
|
|
||||||
port: u16,
|
|
||||||
username: String,
|
|
||||||
password: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ImapReader {
|
|
||||||
pub fn new(host: String, port: u16, username: String, password: String) -> Self {
|
|
||||||
ImapReader {
|
|
||||||
host,
|
|
||||||
port,
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn connect(&self) -> Result<HashMap<String, Vec<u8>>, Box<dyn Error>> {
|
|
||||||
let mut session = self.open_session()?;
|
|
||||||
|
|
||||||
session.examine("INBOX")?;
|
|
||||||
|
|
||||||
let items = match session.uid_search("ALL") {
|
|
||||||
Ok(i) => i,
|
|
||||||
Err(e) => return Err(Box::new(e)),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut msgs = HashMap::<String, Vec<u8>>::with_capacity(items.len());
|
|
||||||
|
|
||||||
//println!("# of messages: {}", &items.len());
|
|
||||||
|
|
||||||
for item in items {
|
|
||||||
let msg = session.uid_fetch(&item.to_string(), "(BODY.PEEK[] UID)")?;
|
|
||||||
|
|
||||||
let message = if let Some(m) = msg.iter().next() {
|
|
||||||
m
|
|
||||||
} else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
let body = message.body().expect("Message did not have a body.");
|
|
||||||
|
|
||||||
msgs.insert(item.to_string(), body.to_owned());
|
|
||||||
}
|
|
||||||
|
|
||||||
session.logout().expect("Could not log out");
|
|
||||||
|
|
||||||
Ok(msgs)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn open_session(
|
|
||||||
&self,
|
|
||||||
) -> Result<
|
|
||||||
Session<
|
|
||||||
rustls_connector::rustls::StreamOwned<
|
|
||||||
rustls_connector::rustls::ClientConnection,
|
|
||||||
TcpStream,
|
|
||||||
>,
|
|
||||||
>,
|
|
||||||
Box<dyn Error + 'static>,
|
|
||||||
> {
|
|
||||||
let stream = TcpStream::connect((self.host.as_ref(), self.port))?;
|
|
||||||
let tls = RustlsConnector::new_with_webpki_roots_certs();
|
|
||||||
let tls_stream = tls.connect(&self.host, stream)?;
|
|
||||||
|
|
||||||
let client = imap::Client::new(tls_stream);
|
|
||||||
|
|
||||||
Ok(client
|
|
||||||
.login(&self.username, &self.password)
|
|
||||||
.map_err(|e| e.0)?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EmailReader for ImapReader {
|
|
||||||
fn read_rfc822_messages(&mut self) -> Box<IntoIter<Message>> {
|
|
||||||
let msgs = match self.connect() {
|
|
||||||
Ok(m) => m,
|
|
||||||
Err(e) => {
|
|
||||||
dbg!(e);
|
|
||||||
return Box::new(Vec::new().into_iter());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let items = msgs
|
|
||||||
.iter()
|
|
||||||
.map(|i| Message::new(i.0.to_owned(), i.1.to_owned()));
|
|
||||||
|
|
||||||
let iter = items.collect::<Vec<Message>>().into_iter();
|
|
||||||
|
|
||||||
Box::new(iter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
use std::{
|
||||||
|
fs::{read_dir, DirEntry},
|
||||||
|
path::PathBuf,
|
||||||
|
vec::IntoIter,
|
||||||
|
};
|
||||||
|
use crate::message::Message;
|
||||||
|
use crate::message_reader::EmailReader;
|
||||||
|
|
||||||
|
pub(crate) struct DataDirectoryMessageReader {
|
||||||
|
path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DataDirectoryMessageReader {
|
||||||
|
pub fn new(path: PathBuf) -> Self {
|
||||||
|
DataDirectoryMessageReader { path }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EmailReader for DataDirectoryMessageReader {
|
||||||
|
fn read_rfc822_messages(&mut self) -> Box<IntoIter<Message>> {
|
||||||
|
println!("Reading files in {}", &self.path.display());
|
||||||
|
let reader = match read_dir(&self.path) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => {
|
||||||
|
dbg!(e);
|
||||||
|
return Box::new(Vec::new().into_iter());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let items = reader
|
||||||
|
.filter(|i| i.is_ok())
|
||||||
|
.map(|i| i.unwrap() as DirEntry)
|
||||||
|
.filter(|d| match d.path().extension() {
|
||||||
|
Some(ext) => ext == "eml",
|
||||||
|
None => false,
|
||||||
|
})
|
||||||
|
.filter_map(|i| {
|
||||||
|
let uid = i
|
||||||
|
.path()
|
||||||
|
.file_stem()
|
||||||
|
.unwrap()
|
||||||
|
.to_owned()
|
||||||
|
.into_string()
|
||||||
|
.expect("Could not convert filename to string.")
|
||||||
|
.split('_')
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.trim_start_matches('0')
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
if let Ok(data) = std::fs::read(i.path()) {
|
||||||
|
Some((uid, data))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(|i| Message::new(i.0, i.1));
|
||||||
|
|
||||||
|
let iter = items.collect::<Vec<Message>>().into_iter();
|
||||||
|
|
||||||
|
Box::new(iter)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,104 @@
|
||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
error::Error,
|
||||||
|
net::TcpStream,
|
||||||
|
vec::IntoIter,
|
||||||
|
};
|
||||||
|
|
||||||
|
use imap::Session;
|
||||||
|
use rustls_connector::RustlsConnector;
|
||||||
|
use crate::message::Message;
|
||||||
|
use crate::message_reader::EmailReader;
|
||||||
|
|
||||||
|
pub struct ImapReader {
|
||||||
|
host: String,
|
||||||
|
port: u16,
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImapReader {
|
||||||
|
pub fn new(host: String, port: u16, username: String, password: String) -> Self {
|
||||||
|
ImapReader {
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn connect(&self) -> Result<HashMap<String, Vec<u8>>, Box<dyn Error>> {
|
||||||
|
let mut session = self.open_session()?;
|
||||||
|
|
||||||
|
session.examine("INBOX")?;
|
||||||
|
|
||||||
|
let items = match session.uid_search("ALL") {
|
||||||
|
Ok(i) => i,
|
||||||
|
Err(e) => return Err(Box::new(e)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut msgs = HashMap::<String, Vec<u8>>::with_capacity(items.len());
|
||||||
|
|
||||||
|
//println!("# of messages: {}", &items.len());
|
||||||
|
|
||||||
|
for item in items {
|
||||||
|
let msg = session.uid_fetch(&item.to_string(), "(BODY.PEEK[] UID)")?;
|
||||||
|
|
||||||
|
let message = if let Some(m) = msg.iter().next() {
|
||||||
|
m
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let body = message.body().expect("Message did not have a body.");
|
||||||
|
|
||||||
|
msgs.insert(item.to_string(), body.to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
session.logout().expect("Could not log out");
|
||||||
|
|
||||||
|
Ok(msgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_session(
|
||||||
|
&self,
|
||||||
|
) -> Result<
|
||||||
|
Session<
|
||||||
|
rustls_connector::rustls::StreamOwned<
|
||||||
|
rustls_connector::rustls::ClientConnection,
|
||||||
|
TcpStream,
|
||||||
|
>,
|
||||||
|
>,
|
||||||
|
Box<dyn Error + 'static>,
|
||||||
|
> {
|
||||||
|
let stream = TcpStream::connect((self.host.as_ref(), self.port))?;
|
||||||
|
let tls = RustlsConnector::new_with_webpki_roots_certs();
|
||||||
|
let tls_stream = tls.connect(&self.host, stream)?;
|
||||||
|
|
||||||
|
let client = imap::Client::new(tls_stream);
|
||||||
|
|
||||||
|
Ok(client
|
||||||
|
.login(&self.username, &self.password)
|
||||||
|
.map_err(|e| e.0)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EmailReader for ImapReader {
|
||||||
|
fn read_rfc822_messages(&mut self) -> Box<IntoIter<Message>> {
|
||||||
|
let msgs = match self.connect() {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(e) => {
|
||||||
|
dbg!(e);
|
||||||
|
return Box::new(Vec::new().into_iter());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let items = msgs
|
||||||
|
.iter()
|
||||||
|
.map(|i| Message::new(i.0.to_owned(), i.1.to_owned()));
|
||||||
|
|
||||||
|
let iter = items.collect::<Vec<Message>>().into_iter();
|
||||||
|
|
||||||
|
Box::new(iter)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,42 @@
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Create newsletter feed
|
Description=Create newsletter feed
|
||||||
|
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=oneshot
|
Type=oneshot
|
||||||
WorkingDirectory=/home/n2w
|
WorkingDirectory=/home/n2w/n2w
|
||||||
ExecStart=/home/n2w/build-feed.sh
|
ExecStart=/home/n2w/build-feed.sh
|
||||||
|
User=n2w
|
||||||
|
|
||||||
|
# Security
|
||||||
|
PrivateTmp=yes
|
||||||
|
PrivateDevices=yes
|
||||||
|
ProtectSystem=strict
|
||||||
|
SystemCallFilter=@system-service
|
||||||
|
#SystemCallFilter=@basic-io @file-system @network-io mprotect
|
||||||
|
CapabilityBoundingSet=
|
||||||
|
NoNewPrivileges=yes
|
||||||
|
ProtectProc=invisible
|
||||||
|
RemoveIPC=yes
|
||||||
|
RestrictAddressFamilies=AF_INET AF_INET6
|
||||||
|
RestrictNamespaces=yes
|
||||||
|
PrivateUsers=yes
|
||||||
|
|
||||||
|
# ProtectHostname and ProcSubset=pid cannot go together
|
||||||
|
# see: https://github.com/systemd/systemd/pull/22203
|
||||||
|
# This is fixed in systemd v251
|
||||||
|
#ProtectHostname=yes
|
||||||
|
ProtectClock=yes
|
||||||
|
ProtectKernalTunables=yes
|
||||||
|
ProtectKernelModules=yes
|
||||||
|
ProtectKernelLogs=yes
|
||||||
|
ProtectControlGroups=yes
|
||||||
|
LockPersonality=yes
|
||||||
|
MemoryDenyWriteExecute=yes
|
||||||
|
RestrictRealtime=yes
|
||||||
|
ProcSubset=pid
|
||||||
|
UMask=0077
|
||||||
|
SystemCallArchitectures=native
|
||||||
|
RestrictSUIDSGID=yes
|
||||||
|
|
Loading…
Reference in New Issue