Compare commits

..

8 Commits

Author SHA1 Message Date
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
8 changed files with 108 additions and 37 deletions

View File

@ -19,7 +19,7 @@ local add_build_steps() = [
volumes: [
{
name: 'dockersock',
path: '/var/run/docker.sock',
path: '/var/run',
},
],
commands: [
@ -29,7 +29,7 @@ local add_build_steps() = [
'rm -rf target/' + arch.target + '/release/*',
],
environment: {
CROSS_DOCKER_IN_DOCKER: true,
CROSS_REMOTE: true,
},
depends_on: ['Wait for Docker'],
}
@ -57,11 +57,11 @@ local add_build_steps() = [
'docker pull hello-world:latest',
],
environment: {
CROSS_DOCKER_IN_DOCKER: true,
CROSS_REMOTE: true,
},
volumes: [{
name: 'dockersock',
path: '/var/run/docker.sock',
path: '/var/run',
}],
}] +
add_build_steps() +
@ -92,12 +92,26 @@ local add_build_steps() = [
},
],
services: [{
name: 'docker',
image: 'docker:dind',
privileged: true,
volumes: [
{
name: 'dockersock',
path: '/var/run',
},
{
name: 'docker-storage',
path: '/var/lib/docker',
},
],
}],
volumes: [
{
name: 'dockersock',
host: {
path: '/var/run/docker.sock',
},
temp: {},
},
],

2
Cargo.lock generated
View File

@ -29,7 +29,7 @@ checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
[[package]]
name = "atom_syndication"
version = "0.11.0"
source = "git+https://github.com/jacobkiers/atom-syndication?rev=525c41508db23634c565ea312bfedb51b5bd3deb#525c41508db23634c565ea312bfedb51b5bd3deb"
source = "git+https://github.com/jacobkiers/atom-syndication?rev=add7083b56b9d737f0fa1d3383aa82789b6c38ad#add7083b56b9d737f0fa1d3383aa82789b6c38ad"
dependencies = [
"chrono",
"derive_builder",

View File

@ -21,4 +21,4 @@ rustls-connector = { version = "^0.16.1", default-features = false, features = [
sha2 = "^0.10.2"
[patch.crates-io]
atom_syndication = { git = "https://github.com/jacobkiers/atom-syndication", rev = "525c41508db23634c565ea312bfedb51b5bd3deb" }
atom_syndication = { git = "https://github.com/jacobkiers/atom-syndication", rev = "add7083b56b9d737f0fa1d3383aa82789b6c38ad" }

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

@ -26,6 +26,7 @@
</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>

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};
@ -37,10 +38,7 @@ pub(crate) fn add_entry_to_feed(
email: from.address.as_ref().map(|e| e.to_string()),
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()),
id: url.clone(),
published: Utc.timestamp_opt(date.to_timestamp(), 0).unwrap(),
@ -79,6 +77,19 @@ pub(crate) fn build_atom_feed(hostname: &String, feed_file: &str) -> 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(out, config)
}
//#[derive(Serialize, Deserialize, Debug)]
pub(crate) struct Newsletter {
id: String,

View File

@ -41,7 +41,11 @@ fn main() -> Result<(), Box<dyn Error>> {
username.to_owned(),
password.to_owned(),
),
cli::Command::BuildFeed { filename, hostname, include_html } => build_feed(filename, hostname, *include_html),
cli::Command::BuildFeed {
filename,
hostname,
include_html,
} => build_feed(filename, hostname, *include_html),
_ => unimplemented!("This method is not yet implemented."),
};
@ -56,7 +60,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()
@ -71,8 +79,10 @@ fn build_feed(filename: &PathBuf, hostname: &String, include_html: bool) -> Resu
create_directory(dir)?;
let feed_file = filename
.file_name().expect("Feed path should have a file name")
.to_str().expect("Feed path should be printable.");
.file_name()
.expect("Feed path should have a file name")
.to_str()
.expect("Feed path should be printable.");
let mut feed = feed::build_atom_feed(&hostname, feed_file);
@ -99,7 +109,7 @@ fn build_feed(filename: &PathBuf, hostname: &String, include_html: bool) -> Resu
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())?;
}
@ -110,10 +120,12 @@ fn build_feed(filename: &PathBuf, hostname: &String, include_html: bool) -> Resu
feed.set_updated(Utc::now());
println!("Writing feed to {}", filename.display());
feed.set_stylesheet(Some("feed.xsl".into()));
write_file(filename, feed.to_string())?;
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))?;
write_file(
dir.join("index.html"),
INDEX_HTML.replace("{FEED}", feed_file),
)?;
}
println!("Finished building the feed.");
@ -132,12 +144,7 @@ fn fetch_from_imap(
print!("Getting mail from {} for mailbox {}", server, username);
let mut reader = ImapReader::new(
server,
port,
username,
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!(
@ -175,8 +182,6 @@ fn fetch_from_imap(
Ok(())
}
fn get_path(parsed: &ParsedMessage, msg: &Message) -> String {
let date = parsed.date().expect("Could not extract date");
let date_str = format!(
@ -188,7 +193,8 @@ fn get_path(parsed: &ParsedMessage, msg: &Message) -> String {
parsed.body_html(0).expect("Expected a body").as_bytes(),
));
let uid: i32 = msg.get_uid()
let uid: i32 = msg
.get_uid()
.parse()
.unwrap_or_else(|_| panic!("Could not convert message id {} to an i32.", msg.get_uid()));
@ -199,13 +205,17 @@ fn process_html(input: &str) -> Result<String, ()> {
Ok(input.replace("src", "data-source"))
}
fn write_file<P: Into<PathBuf>, D: AsRef<[u8]>>(html_path: P, data: D) -> Result<(), std::io::Error> {
let path : PathBuf = html_path.into();
fn open_file<P: Into<PathBuf>>(path: P) -> std::io::Result<std::fs::File> {
OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(&path)
.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
}
}
}