commit ec46a11cc6d9b99349f4b46f3270a06b6df7b930 Author: Konstantin Nazarov Date: Tue Apr 13 20:44:09 2021 +0000 Initial verion of notes.sh diff --git a/README.md b/README.md new file mode 100644 index 0000000..69b0e11 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# notes.sh: plain-text notes with IMAP synchronization + +This project allows you to keep plaintext notes, and attach files to them (like images, PDFs, etc). The plaintext stays plaintext, easily greppable, and with every note in its own file. + +What makes it special is that it uses a Maildir format to store notes. This allows you to easily sync your notes with any mail server, and have your notes accessible on the go through your email client. + +NB: This is alpha software, use with caution. diff --git a/notes.sh b/notes.sh new file mode 100755 index 0000000..52c8842 --- /dev/null +++ b/notes.sh @@ -0,0 +1,227 @@ +#!/bin/bash + +set -e + +DATE_FORMAT="%a, %d %b %Y %H:%M:%S %z" +BASEDIR=~/Maildir/personal/Notes +PID=$$ + + +if [ ! -d "$BASEDIR" ]; then + mkdir -p "$BASEDIR"/{tmp,new,cur} +fi + +uuid() +{ + local N B T + + for (( N=0; N < 16; ++N )) + do + B=$(( $RANDOM%255 )) + + if (( N == 6 )) + then + printf '4%x' $(( B%15 )) + elif (( N == 8 )) + then + local C='89ab' + printf '%c%x' ${C:$(( $RANDOM%${#C} )):1} $(( B%15 )) + else + printf '%02x' $B + fi + + for T in 3 5 7 9 + do + if (( T == N )) + then + printf '-' + break + fi + done + done + + echo +} + +get_header() { + FILE="$1" + HEADER="$2" + echo $(grep -m1 "^${HEADER}: " < "$FILE" | sed -n "s/^${HEADER}: \(.*\)$/\1/p") +} + +get_part() { + FILE="$1" + BOUNDARY="$2" + NUM="$3" + FILTER="\ + BEGIN { \ + rec=0; \ + body=0; \ + } \ + { \ + if (\$0==\"--$BOUNDARY\" || \$0==\"--$BOUNDARY--\") { \ + if (body == 0) { \ + body=1; \ + } \ + rec=rec+1; \ + } \ + else if (body == 1 && rec==$NUM) { \ + print \$0 \ + } \ + }" + + awk "$FILTER" "$FILE" +} + +unpack_part() { + FILE="$1" + DIR="$2" + MIME_TYPE=$(get_header "$FILE" Content-Type) + DISPOSITION=$(get_header "$FILE" Content-Disposition) + + if [[ $DISPOSITION == *"attachment"* ]]; then + ENCODING=$(get_header "$FILE" Content-Transfer-Encoding) + FILENAME=$(echo $DISPOSITION | \ + sed -n 's/^.*filename="\{0,1\}\(.*\)"\{0,1\}$/\1/p') + FILTER="\ + { \ + if (body==1) {print \$0}\ + if (\$0~/^$/) {body=1} \ + } \ + " + + if [[ $ENCODING == *"base64"* ]]; then + awk "$FILTER" "$FILE" | base64 --decode >> "$DIR/$FILENAME" + else + awk "$FILTER" "$FILE" >> "$DIR/$FILENAME" + fi + elif [[ $MIME_TYPE == *"text/plain"* ]]; then + FILTER="\ + { \ + if (body==1) {print \$0}\ + if (\$0~/^$/) {body=1} \ + } \ + " + awk "$FILTER" "$FILE" >> "$DIR/note.md" + elif [[ $MIME_TYPE == *"multipart/mixed"* ]]; then + BOUNDARY=$(echo $MIME_TYPE | sed -n 's/^.*boundary="\(.*\)"$/\1/p') + i=1 + while true; do + TMP=$(mktemp --tmpdir="$DIR") + get_part "$FILE" "$BOUNDARY" "$i" > "$TMP" + ((i++)) + if [ ! -s "$TMP" ]; then + rm "$TMP" + break + fi + #cat "$TMP" + (unpack_part "$TMP" "$DIR") + rm "$TMP" + done + elif [[ $MIME_TYPE == *"multipart/related"* ]]; then + echo "multipart/related not yet supported" + exit 1 + fi +} + + + +unpack_mime() { + FILE="$1" + DIR="$2" + + DATE=$(get_header "$FILE" Date) + SUBJECT=$(get_header "$FILE" Subject) + MIME_TYPE=$(get_header "$FILE" Content-Type) + MESSAGE_ID=$(get_header "$FILE" Message-Id) + + echo "Date: $DATE" > "$DIR/note.md" + if [ ! -z "$MESSAGE_ID" ]; then + echo "Message-Id: $MESSAGE_ID" >> "$DIR/note.md" + fi + echo "Subject: $SUBJECT" >> "$DIR/note.md" + echo "" >> "$DIR/note.md" + + TMP=$(mktemp --tmpdir="$DIR") + cat "$FILE" > "$TMP" + + (unpack_part "$TMP" "$DIR") + rm "$TMP" +} + +new_entry() { + ENTRY_FILE=$(mktemp) + + vim -c ":set syntax=markdown" \ + -c "1 s/^/# /" \ + -c "norm $" \ + "$ENTRY_FILE" + + if [ -s "$ENTRY_FILE" ]; then + MIME_TIMESTAMP=$(LC_ALL="en_US.UTF-8" date "+$DATE_FORMAT") + UNIX_TIMESTAMP=$(date "+%s") + HOSTNAME=$(hostname -s) + SUBJECT=$(head -1 "$ENTRY_FILE") + SUBJECT=${SUBJECT#"# "} + + RESULT=$(mktemp) + echo "Date: $MIME_TIMESTAMP" >> "$RESULT" + echo "MIME-Version: 1.0" >> "$RESULT" + echo "Content-Type: text/plain; charset=utf-8" >> "$RESULT" + echo "Content-Disposition: inline" >> "$RESULT" + echo "Message-Id: <$(uuid)@notes.sh>" >> "$RESULT" + echo "Subject: $SUBJECT" >> "$RESULT" + echo "" >> "$RESULT" + sed "1d" < "$ENTRY_FILE" >> "$RESULT" + + #cat "$RESULT" + + FILENAME="$UNIX_TIMESTAMP.${PID}_1.${HOSTNAME}:2,S" + mv "$RESULT" "$BASEDIR/cur/$FILENAME" + + echo "done" + fi +} + +edit_entry() { + unpack_mime "$1" ~/scratch/notes +} + +list_entries() { + grep -m1 -r "^Subject:" "$BASEDIR" | sed -n 's/^\(.*\):Subject: \(.*\)$/\1:\2/p' + +} + +usage() { + echo "$0 {--new,--list,--edit,--help}" +} + + +while (( "$#" )); do + case "$1" in + -n|--new) + new_entry + exit 0 + ;; + -l|--list) + list_entries + exit 0 + ;; + -e|--edit) + if [ -z "$2" ]; then + echo "Misssing argument for $1" + exit 1 + fi + edit_entry "$2" + exit 0 + ;; + *) + usage + exit 1 + ;; + esac + +done + +usage +exit 1