Compare commits

...

29 Commits

Author SHA1 Message Date
Jacob Kiers 9f815f3f9c Release version 0.2.3
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2022-12-29 18:56:26 +01:00
Jacob Kiers 6cb35aae42 Update dependencies with contributions
Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2022-12-29 18:55:19 +01:00
Jacob Kiers 7c5e971571 Merge pull request 'Add `update` subcommand to update itself to the latest version' (#16) from 15-update into master
continuous-integration/drone/push Build is passing Details
2022-12-28 23:16:41 +00:00
Jacob Kiers 0598026756 Add update subcommand
continuous-integration/drone/push Build is passing Details
This allows updating the binary to the latest verison.

Closes: #15

Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2022-12-29 00:08:49 +01:00
Jacob Kiers 4069d8ac31 Update changelog
continuous-integration/drone/push Build is passing Details
Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2022-12-27 23:00:45 +01:00
Jacob Kiers 7407654e60 Fix CS and some other small things
Useful tool, that clippy...

Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2022-12-27 23:00:45 +01:00
Jacob Kiers e7fd41ff95 Truncate the feed file before writing
Otherwise, the feed may be overwritten, but if it is shorter, it may
contain some leftover data from a previous run. In that case, the file
will be invalid XML, thereby failing to be parsed.

Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2022-12-27 23:00:45 +01:00
Jacob Kiers c2d09621aa Add style sheet to the feed
Based on the code proposed in PR 70 of the atom-syndication crate.

Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2022-12-27 22:59:45 +01:00
Jacob Kiers 9129f7e11b Put real feed url into feed
Instead of hardcoding the feed file name to be feed.atom, it has been
configurable for a while. This is now also reflected in the feed itself.

Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2022-12-27 22:50:50 +01:00
Jacob Kiers 71371cb3e1 Go back to docker-in-docker
continuous-integration/drone/push Build is passing Details
Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2022-12-27 21:07:41 +01:00
Jacob Kiers f61e635721 Add link to releases page
continuous-integration/drone/push Build is passing Details
Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2022-12-27 12:27:27 +01:00
Jacob Kiers d3e4c9e790 Add usage instruction to README
continuous-integration/drone/push Build is passing Details
Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2022-12-27 12:21:21 +01:00
Jacob Kiers 3d7e5fc2cf Update to v0.2.2
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2022-12-16 19:18:43 +01:00
Jacob Kiers 78049cf7b6 Remove unnecessary docker-in-docker
continuous-integration/drone/push Build is passing Details
We're now launching new build servers, so security is less of a concern.

Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2022-12-16 19:01:43 +01:00
Jacob Kiers 3abec884c2 Really remove targets I don't care about
continuous-integration/drone/push Build is passing Details
Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2022-12-16 18:43:56 +01:00
Jacob Kiers ea30a49901 Build with rust v1.66 and cross v0.2.4
Also added some sanity checks. The new docker image now also contains
all required targets and the rust-src component already, which makes it
a bit faster to get started.

Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2022-12-16 18:42:38 +01:00
Jacob Kiers 31fda8bae2 Update changelog for v0.2.1
continuous-integration/drone/push Build was killed Details
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone Build encountered an error Details
Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2022-12-13 17:01:09 +01:00
Jacob Kiers b19688a79c Remove builds I don't personally care about
continuous-integration/drone/push Build is failing Details
continuous-integration/drone Build is passing Details
Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2022-12-13 16:05:39 +01:00
Jacob Kiers 546c063ce9 Update to mail_parser v0.8.0
continuous-integration/drone Build was killed Details
Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2022-12-13 15:44:14 +01:00
Jacob Kiers 751b4d2551 Release version 0.2.0.
continuous-integration/drone/push Build was killed Details
continuous-integration/drone/tag Build was killed Details
Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2022-11-30 09:16:11 +01:00
Jacob Kiers 60362ff7c2 Add style sheet to feed
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build encountered an error Details
This makes it easier in a browser to use and understand the feed.

The style sheet is shamelessly copied from
https://andrewstiefel.com/style-atom-xsl/

Closes #4

Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2022-11-29 23:55:54 +01:00
Jacob Kiers 4599ce47fb Replace hard-coded feed name with configured name
Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2022-11-29 23:22:43 +01:00
Jacob Kiers 6d3527d3e4 Add some cargo instructions
For example, to derive the documetation from the README.md file.

Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2022-11-29 23:09:06 +01:00
Jacob Kiers 49a9670c4a Rename feed by default to feed.xml
This helps with processing in browsers.

Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2022-11-29 23:08:12 +01:00
Jacob Kiers 0f65550169 Write index.html file to output
This gives a simple landing page with a rel=alternate link to the feed.

Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2022-11-29 23:07:11 +01:00
Jacob Kiers 7658748fa7 Build with rust 1.65
continuous-integration/drone/push Build is passing Details
Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2022-11-29 22:33:43 +01:00
Jacob Kiers a4d9d16482 Update rust dependencies
Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2022-11-29 22:33:31 +01:00
Jacob Kiers 427831fc93 Update dependencies
continuous-integration/drone/push Build is passing Details
Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2022-11-10 20:06:34 +01:00
Jacob Kiers 9a89febcb2 Remove workspace
Since only the bin crate was left, there is no reason to keep the
workspace setup.

Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2022-11-10 20:04:35 +01:00
17 changed files with 1199 additions and 239 deletions

3
.cargo/config.toml Normal file
View File

@ -0,0 +1,3 @@
[profile.release]
lto = "thin"
strip = true

View File

@ -1,31 +1,26 @@
local executableName = 'newsletter-to-web';
local cross_image = 'img.kie.rs/jjkiers/rust-dind-cross:1.66-full';
local archs = [
{ target: 'aarch64-unknown-linux-gnu', short: 'arm64-gnu' },
{ target: 'aarch64-unknown-linux-musl', short: 'arm64-musl' },
{ target: 'x86_64-pc-windows-gnu', short: 'windows' },
{ target: 'x86_64-unknown-linux-gnu', short: 'amd64-gnu' },
{ target: 'x86_64-unknown-linux-musl', short: 'amd64-musl' },
];
local getStepName(arch) = 'Build for ' + arch.short;
local builtExecutableName(arch) = executableName + if std.startsWith(arch.short, 'windows') then '.exe' else '';
local targetExecutableName(arch) = executableName + if std.startsWith(arch.short, 'windows') then '.exe' else '-' + arch.short;
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 add_build_steps() = [
{
name: getStepName(arch),
image: 'img.kie.rs/jjkiers/rust-dind-cross:1.62-slim',
image: cross_image,
volumes: [
{
name: 'dockersock',
path: '/var/run',
},
{
name: 'rustup',
path: '/usr/local/rustup',
},
],
commands: [
'echo Hello World from Jsonnet on ' + arch.target + '!',
@ -51,13 +46,19 @@ local add_build_steps() = [
steps:
[{
name: 'Wait for Docker',
image: 'img.kie.rs/jjkiers/rust-dind-cross:1.62-slim',
image: cross_image,
commands: [
'mkdir artifacts',
'echo Using image: ' + cross_image,
'while ! docker image ls; do sleep 1; done',
'cargo --version',
'rustc --version',
'docker info',
'docker pull hello-world:latest',
'mkdir artifacts',
],
environment: {
CROSS_REMOTE: true,
},
volumes: [{
name: 'dockersock',
path: '/var/run',
@ -67,7 +68,7 @@ local add_build_steps() = [
[
{
name: 'Show built artifacts',
image: 'img.kie.rs/jjkiers/rust-dind-cross:1.62-slim',
image: cross_image,
commands: [
'ls -lah artifacts',
],
@ -112,18 +113,6 @@ local add_build_steps() = [
name: 'dockersock',
temp: {},
},
{
name: 'docker-storage',
host: {
path: '/srv/drone/docker-dind-rust',
},
},
{
name: 'rustup',
host: {
path: '/srv/drone/rustup',
},
},
],
image_pull_secrets: ['docker_private_repo'],

44
CHANGELOG.md Normal file
View File

@ -0,0 +1,44 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.2.3] - 2022-12-29
### Added
* Added `update` subcommand to update to the latest version.
### Fixed
* Truncate feed file before writing, to prevent corruption from leftover data.
* Ensure the feed file name is part of the self URL. This was still hardcoded to `feed.atom`.
## [0.2.2] - 2022-12-16
### Changed
* Updated build pipeline to generate much smaller binaries
## [0.2.1] - 2022-12-13
### Changed
* Updated the [mail-parser](https://docs.rs/crate/mail-parser/0.8.0) crate to v0.8.0.
* Removed two builds from the build pipeline.
## [0.2.0] - 2022-11-30
### Added
* A style sheet is added to the feed for easy readability in a web browser (#4).
* The output directory will now contain an index.html file with some information.
### Changed
* By default, the name of the feed is now feed.xml instead of feed.atom.

1003
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,25 @@
[workspace]
members = [
"bin",
[package]
name = "newsletter-to-web"
version = "0.2.3"
edition = "2021"
description = "Converts email newsletters to static HTML files"
homepage = "https://code.kiers.eu/newsletter-to-web/newsletter-to-web"
authors = [
"Jacob Kiers <code@kiers.eu>"
]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
atom_syndication = "^0.11.0"
base16ct = { version = "^0.1.0", features = [ "alloc" ] }
chrono = "^0.4"
clap = { version = "^4.0.22", features = [ "derive" ] }
imap = { version = "^2.4.1", default-features = false }
mail-parser = "^0.8.0"
rustls-connector = { version = "^0.16.1", default-features = false, features = [ "webpki-roots-certs", "quic" ] }
sha2 = "^0.10.2"
self_update = { version = "0.33.0", default-features = false, features = ["rustls"] }
[patch.crates-io]
atom_syndication = { git = "https://github.com/rust-syndication/atom", rev = "5cf8d161e5e5af7d93cca8d2c117b7af879a99b7" }

View File

@ -1,6 +1,41 @@
# Newsletter 2 Web
# Newsletter to Web
Converts a newsletter to static HTML files.
Converts a newsletter to and Atom feed and static HTML files.
## Usage
Get the latest release [from the releases page](https://code.kiers.eu/newsletter-to-web/newsletter-to-web/releases/latest).
### Getting help
For help, use
* `newsletter-to-web help`
* `newsletter-to-web help <subcommand>`.
### Basic usage
First, download all messages from the IMAP mail server
and store them in the `data/` directory:
```sh
newsletter-to-web fetch-from-imap -s <imap.example.com> -u <email@example.com> -p <password>
```
Then, convert them to an Atom feed, using
`newsletters.example.com` as the base domain:
```sh
newsletter-to-web --include-html build-feed newsletters.example.org
```
This will put the output in the `output/` directory. The Atom
feed will be in `output/feed.xml`, together with a very simple
`index.html` file pointing to the feed. It will also add an HTML
file for every email with the HTML content.
The feed will already contain the full HTML, so it can easily be
read from a feed reader.
## Features

View File

@ -1,20 +0,0 @@
[package]
name = "newsletter-to-web"
version = "0.1.0"
edition = "2021"
description = "Converts email newsletters to static HTML files"
authors = [
"Jacob Kiers <code@kiers.eu>"
]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
atom_syndication = "^0.11.0"
base16ct = { version = "^0.1.0", features = [ "alloc" ] }
chrono = "^0.4"
clap = { version = "^3.2.16", features = [ "derive" ] }
imap = { version = "^2.4.1", default-features = false }
mail-parser = "^0.5.0"
rustls-connector = { version = "^0.16.1", default-features = false, features = [ "webpki-roots-certs", "quic" ] }
sha2 = "^0.10.2"

78
resources/feed.xsl Normal file
View File

@ -0,0 +1,78 @@
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:atom="http://www.w3.org/2005/Atom"
exclude-result-prefixes="atom"
>
<xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="/">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"/>
<title>Web Feed • <xsl:value-of select="atom:feed/atom:title"/></title>
<style type="text/css">
body{max-width:768px;margin:0 auto;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:16px;line-height:1.5em}section{margin:30px 15px}h1{font-size:2em;margin:.67em 0;line-height:1.125em}h2{border-bottom:1px solid #eaecef;padding-bottom:.3em}.alert{background:#fff5b1;padding:4px 12px;margin:0 -12px}a{text-decoration:none}.entry h3{margin-bottom:0}.entry p{margin:4px 0}
</style>
</head>
<body>
<section>
<div class="alert">
<p><strong>This is a web feed</strong>, also known as an RSS feed. <strong>Subscribe</strong> by copying the URL from the address bar into your newsreader app.</p>
</div>
</section>
<section>
<xsl:apply-templates select="atom:feed" />
</section>
<section>
<h2>Recent Items</h2>
<p>Last updated on <xsl:apply-templates select="atom:feed/atom:updated" /></p>
<xsl:apply-templates select="atom:feed/atom:entry" />
</section>
</body>
</html>
</xsl:template>
<xsl:template match="atom:feed">
<h1><xsl:value-of select="atom:title"/>'s Web Feed Preview</h1>
<p>This RSS feed provides the latest posts from <xsl:value-of select="atom:title"/>'s blog.
<a class="head_link" target="_blank">
<xsl:attribute name="href">
<xsl:value-of select="atom:link[@rel='alternate']/@href"/>
</xsl:attribute>
Visit Website &#x2192;
</a>
</p>
<h2>What is an RSS feed?</h2>
<p>An RSS feed is a data format that contains the latest content from a website, blog, or podcast. You can use feeds to <strong>subscribe</strong> to websites and get the <strong>latest content in one place</strong>.</p>
<ul>
<li><strong>Feeds put you in control.</strong> Unlike social media apps, there is no algorithm deciding what you see or read. You always get the latest content from the creators you care about.</li>
<li><strong>Feed are private by design.</strong> No one owns web feeds, so no one is harvesting your personal information and profiting by selling it to advertisers.</li>
<li><strong>Feeds are spam-proof.</strong> Had enough? Easy, just unsubscribe from the feed.</li>
</ul>
<p>All you need to do to get started is to add the URL (web address) for this feed to a special app called a newsreader. Visit <a href="https://aboutfeeds.com/">About Feeds</a> to get started with newsreaders and subscribing. Its free. </p>
</xsl:template>
<xsl:template match="atom:entry">
<div class="entry">
<h3>
<a target="_blank">
<xsl:attribute name="href">
<xsl:value-of select="atom:id"/>
</xsl:attribute>
<xsl:value-of select="atom:title"/>
</a>
</h3>
<p>
<xsl:value-of select="atom:summary" disable-output-escaping="yes" />
</p>
<small>
Published: <xsl:value-of select="atom:updated" />
</small>
</div>
</xsl:template>
</xsl:stylesheet>

View File

@ -1,8 +1,12 @@
<!DOCTYPE html>
<html>
<head><title>Newsletters</title></head>
<head>
<title>Newsletters</title>
<link rel="alternate" type="application/atom+xml" title="ATOM feed" href="./{FEED}" />
</head>
<body>
<h1>Newsletters 2 Web</h1>
<p>The real feed can be found at <a href="./{FEED}">{FEED}</a></p>
<p>Nothing to see here.</p>
<p>This is a public landing page for an instance of the
<a href="https://code.kiers.eu/newsletter-to-web/newsletter-to-web">Newsletters 2 Web</a>

View File

@ -32,7 +32,7 @@ pub(crate) enum Command {
/// Host name hosting the feed
hostname: String,
/// Feed file
#[clap(value_parser, default_value = "output/feed.atom")]
#[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)]
@ -50,4 +50,6 @@ pub(crate) enum Command {
#[clap(value_parser, default_value = "output/")]
directory: PathBuf,
},
/// Update to the latest version
Update,
}

1
src/command.rs Normal file
View File

@ -0,0 +1 @@
pub(crate) mod update;

17
src/command/update.rs Normal file
View File

@ -0,0 +1,17 @@
use self_update::cargo_crate_version;
use std::error::Error;
pub(crate) fn self_update() -> Result<(), Box<dyn Error>> {
let backend = self_update::backends::gitea::Update::configure()
.with_host("https://code.kiers.eu")
.repo_owner("newsletter-to-web")
.repo_name("newsletter-to-web")
.bin_name("newsletter-to-web")
.show_download_progress(true)
.current_version(cargo_crate_version!())
.build()?;
let status = backend.update()?;
println!("Update status: `{}`!", status.version());
Ok(())
}

View File

@ -2,6 +2,7 @@ use crate::Message;
use atom_syndication::{
ContentBuilder, Entry, EntryBuilder, Feed, FeedBuilder, Generator, LinkBuilder, Person,
WriteConfig,
};
use chrono::{DateTime, TimeZone, Utc};
@ -15,8 +16,8 @@ pub(crate) fn add_entry_to_feed(
include_html: bool,
) {
let parsed = message.get_parsed().unwrap();
let date = parsed.get_date().expect("Could not extract date");
let from = match parsed.get_from() {
let date = parsed.date().expect("Could not extract date");
let from = match parsed.from() {
HeaderValue::Address(e) => e,
_ => return,
};
@ -34,43 +35,38 @@ pub(crate) fn add_entry_to_feed(
_ => "".to_string(),
},
},
email: match &from.address {
Some(e) => Some(e.to_string()),
_ => None,
},
email: from.address.as_ref().map(|e| e.to_string()),
uri: None,
},
title: parsed
.get_subject()
.expect("Expected a subject")
.to_string(),
title: parsed.subject().expect("Expected a subject").to_string(),
content: Some(processed_html.clone()),
id: url.clone(),
published: Utc.timestamp(date.to_timestamp(), 0), //(format!("{}{}", &date.to_iso8601(), "+00:00").as_str()).`unwrap(),
published: Utc.timestamp_opt(date.to_timestamp(), 0).unwrap(),
url: match include_html {
true => url,
false => "".to_string(),
},
}
.into();
entry.set_updated(Utc.timestamp(date.to_timestamp(), 0));
entry.set_updated(Utc.timestamp_opt(date.to_timestamp(), 0).unwrap());
feed.entries.push(entry);
}
pub(crate) fn build_atom_feed(hostname: &String) -> Feed {
pub(crate) fn build_atom_feed(hostname: &String, feed_file: &str) -> Feed {
let feed_url = format!("https://{}/{}", hostname, feed_file);
FeedBuilder::default()
.title("JJKiers Newsletters")
.id(format!("https://{}/feed.atom", hostname))
.id(&feed_url)
.link(
LinkBuilder::default()
.href(format!("https://{}/", hostname))
.rel("alternate")
.rel("alternate".to_string())
.build(),
)
.link(
LinkBuilder::default()
.href(format!("https://{}/feed.atom", hostname))
.rel("self")
.href(&feed_url)
.rel("self".to_string())
.build(),
)
.generator(Generator {
@ -81,6 +77,19 @@ pub(crate) fn build_atom_feed(hostname: &String) -> Feed {
.build()
}
pub(crate) fn write_feed<W: std::io::Write>(
feed: Feed,
mut out: W,
) -> Result<W, atom_syndication::Error> {
let _ = writeln!(out, r#"<?xml version="1.0"?>"#);
let _ = writeln!(out, r#"<?xml-stylesheet href="feed.xsl" type="text/xsl"?>"#);
let config = WriteConfig {
write_document_declaration: false,
..Default::default()
};
feed.write_with_config(out, config)
}
//#[derive(Serialize, Deserialize, Debug)]
pub(crate) struct Newsletter {
id: String,
@ -104,15 +113,15 @@ impl From<Newsletter> for Entry {
eb.title(post.title)
.id(post.id)
.published(Some(post.published.clone().into()))
.author(post.author.into())
.published(Some(post.published.into()))
.author(post.author)
.content(content);
if post.url.len() > 1 {
eb.link(
LinkBuilder::default()
.href(post.url)
.rel("alternate")
.rel("alternate".to_string())
.build(),
);
}

View File

@ -1,4 +1,7 @@
#[warn(missing_docs)]
#[doc = include_str!("../README.md")]
mod cli;
mod command;
mod feed;
mod message;
mod message_reader;
@ -10,13 +13,16 @@ use message_reader::{DataDirectoryMessageReader, EmailReader, ImapReader};
use sha2::{Digest, Sha256};
use std::{
error::Error,
fs::{File, OpenOptions},
fs::OpenOptions,
io::Write,
path::{Path, PathBuf},
};
pub(crate) use message::Message;
const INDEX_HTML: &str = include_str!("../resources/index.html");
const FEED_STYLESHEET: &str = include_str!("../resources/feed.xsl");
fn main() -> Result<(), Box<dyn Error>> {
let cli = cli::Cli::parse();
@ -35,7 +41,12 @@ fn main() -> Result<(), Box<dyn Error>> {
username.to_owned(),
password.to_owned(),
),
cli::Command::BuildFeed { filename , hostname, include_html } => build_feed(&filename, hostname.to_owned(), *include_html),
cli::Command::BuildFeed {
filename,
hostname,
include_html,
} => build_feed(filename, hostname, *include_html),
cli::Command::Update => command::update::self_update(),
_ => unimplemented!("This method is not yet implemented."),
};
@ -50,7 +61,11 @@ fn create_directory<P: AsRef<Path>>(dir: P) -> Result<(), std::io::Error> {
Ok(())
}
fn build_feed(filename: &PathBuf, hostname: String, include_html: bool) -> Result<(), Box<dyn Error>> {
fn build_feed(
filename: &PathBuf,
hostname: &String,
include_html: bool,
) -> Result<(), Box<dyn Error>> {
let dir = filename.parent().ok_or(format!(
"Could not get parent directory of {}",
filename.display()
@ -64,45 +79,54 @@ fn build_feed(filename: &PathBuf, hostname: String, include_html: bool) -> Resul
create_directory(dir)?;
let mut feed = feed::build_atom_feed(&hostname);
let feed_file = filename
.file_name()
.expect("Feed path should have a file name")
.to_str()
.expect("Feed path should be printable.");
let mut reader = DataDirectoryMessageReader::new((&Path::new("data")).to_path_buf());
let mut feed = feed::build_atom_feed(&hostname, feed_file);
let mut reader = DataDirectoryMessageReader::new(Path::new("data").to_path_buf());
for msg in reader.read_rfc822_messages() {
let parsed = msg.get_parsed().expect("A parsed messsage.");
let date = parsed.get_date().ok_or(format!(
let date = parsed.date().ok_or(format!(
"Could not get the date of message {}",
msg.get_uid()
))?;
let subject = match parsed.get_subject() {
Some(subject) => subject,
None => "No subject",
};
let subject = parsed.subject().unwrap_or("No subject");
println!(
"Processing message {} from {} with subject {}",
msg.get_uid(),
date.to_string(),
date,
subject
);
let html_body = parsed.get_html_body(0).expect("Could not read html body");
let html_body = parsed.body_html(0).expect("Could not read html body");
let processed_html = process_html(&html_body).expect("Could not process the 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())?;
}
feed::add_entry_to_feed(&mut feed, &msg, &processed_html, &hostname, include_html);
}
if feed.entries.len() > 0 {
if !feed.entries.is_empty() {
feed.set_updated(Utc::now());
println!("Writing feed to {}", filename.display());
let _ = feed.write_to(File::create(filename).unwrap());
feed::write_feed(feed, open_file(filename).unwrap())?;
write_file(dir.join("feed.xsl"), FEED_STYLESHEET)?;
write_file(
dir.join("index.html"),
INDEX_HTML.replace("{FEED}", feed_file),
)?;
}
println!("Finished building the feed.");
@ -121,12 +145,7 @@ fn fetch_from_imap(
print!("Getting mail from {} for mailbox {}", server, username);
let mut reader = ImapReader::new(
String::from(server),
port,
String::from(username),
String::from(password),
);
let mut reader = ImapReader::new(server, port, username, password);
for msg in reader.read_rfc822_messages() {
let parsed = msg.get_parsed().ok_or(format!(
@ -134,20 +153,17 @@ fn fetch_from_imap(
msg.get_uid()
))?;
let date = parsed.get_date().ok_or(format!(
let date = parsed.date().ok_or(format!(
"Could not get the date of message {}",
msg.get_uid()
))?;
let subject = match parsed.get_subject() {
Some(subject) => subject,
None => "No subject",
};
let subject = parsed.subject().unwrap_or("No subject");
println!(
"Processing message {} from {} with subject {}",
msg.get_uid(),
date.to_string(),
date,
subject
);
@ -167,35 +183,40 @@ fn fetch_from_imap(
Ok(())
}
fn get_path(parsed: &ParsedMessage, msg: &Message) -> String {
let date = parsed.get_date().expect("Could not extract date");
let date = parsed.date().expect("Could not extract date");
let date_str = format!(
"{:04}{:02}{:02}{:02}{:02}{:02}",
&date.year, &date.month, &date.day, &date.hour, &date.minute, &date.second
);
let hash = base16ct::lower::encode_string(&Sha256::digest(
&parsed.get_html_body(0).expect("Expected a body").as_bytes(),
parsed.body_html(0).expect("Expected a body").as_bytes(),
));
let uid: i32 = msg.get_uid()
let uid: i32 = msg
.get_uid()
.parse()
.expect(&format!("Could not convert message id {} to an i32.", msg.get_uid()));
.unwrap_or_else(|_| panic!("Could not convert message id {} to an i32.", msg.get_uid()));
format!("{:05}_{}_{}.html", uid, date_str, &hash).to_owned()
format!("{:05}_{}_{}.html", uid, date_str, &hash)
}
fn process_html(input: &str) -> Result<String, ()> {
Ok(input.replace("src", "data-source"))
}
fn write_file(html_path: &PathBuf, data: &[u8]) -> Result<(), std::io::Error> {
fn open_file<P: Into<PathBuf>>(path: P) -> std::io::Result<std::fs::File> {
OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(&html_path)
.expect(format!("Could not open file '{}' for writing", &html_path.display()).as_str())
.write_all(data)
}
.open(path.into())
}
fn write_file<P: Into<PathBuf>, D: AsRef<[u8]>>(path: P, data: D) -> Result<(), std::io::Error> {
let path: PathBuf = path.into();
open_file(path.clone())
.unwrap_or_else(|_| panic!("Could not open file '{}' for writing", &path.display()))
.write_all(data.as_ref())
}

View File

@ -21,4 +21,4 @@ impl Message {
pub fn get_data(&self) -> &Vec<u8> {
&self.data
}
}
}

View File

@ -44,7 +44,7 @@ impl EmailReader for DataDirectoryMessageReader {
Some(ext) => ext == "eml",
None => false,
})
.map(|i| {
.filter_map(|i| {
let uid = i
.path()
.file_stem()
@ -52,9 +52,9 @@ impl EmailReader for DataDirectoryMessageReader {
.to_owned()
.into_string()
.expect("Could not convert filename to string.")
.split("_")
.split('_')
.collect::<Vec<&str>>()[0]
.trim_start_matches("0")
.trim_start_matches('0')
.to_string();
if let Ok(data) = std::fs::read(i.path()) {
@ -63,8 +63,6 @@ impl EmailReader for DataDirectoryMessageReader {
None
}
})
.filter(|i| i.is_some())
.map(|i| i.unwrap())
.map(|i| Message::new(i.0, i.1));
let iter = items.collect::<Vec<Message>>().into_iter();