stamail

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit cf1a6751aaca3dbb6c8acd16c3d24e917788ee5c
parent 60f4a6c5a5514a61dcbef6ee1ea102be5c669730
Author: Nathaniel Chappelle <nathaniel@chappelle.dev>
Date:   Sat, 24 Jan 2026 14:26:18 -0800

bringing over json, jsmn, and piping glue

Diffstat:
M.gitignore | 2++
AMakefile | 21+++++++++++++++++++++
Ajsmn.h | 473+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ajson.c | 137+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ajson.h | 8++++++++
Astamail.c | 30++++++++++++++++++++++++++++++
6 files changed, 671 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -1,2 +1,4 @@ test/ PROJECT +*.o +stamail diff --git a/Makefile b/Makefile @@ -0,0 +1,21 @@ +CC = cc +CFLAGS = -std=c99 -Wall -Wextra -O2 +OBJ = stamail.o json.o +BIN = stamail + +all: $(BIN) + +$(BIN): $(OBJ) + $(CC) $(CFLAGS) -o $@ $(OBJ) + +stamail.o: stamail.c json.h + $(CC) $(CFLAGS) -c stamail.c + +json.o: json.c json.h jsmn.h + $(CC) $(CFLAGS) -c json.c + +clean: + rm -f $(OBJ) $(BIN) + +.PHONY: all clean + diff --git a/jsmn.h b/jsmn.h @@ -0,0 +1,473 @@ +/* + * jsmn, a minimalistic JSON parser in C. + * + * MIT License + * + * Copyright (c) 2010 Serge Zaitsev + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#ifndef JSMN_H +#define JSMN_H + +#include <stddef.h> + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef JSMN_STATIC +#define JSMN_API static +#else +#define JSMN_API extern +#endif + +/** + * JSON type identifier. Basic types are: + * o Object + * o Array + * o String + * o Other primitive: number, boolean (true/false) or null + */ +typedef enum { + JSMN_UNDEFINED = 0, + JSMN_OBJECT = 1 << 0, + JSMN_ARRAY = 1 << 1, + JSMN_STRING = 1 << 2, + JSMN_PRIMITIVE = 1 << 3 +} jsmntype_t; + +enum jsmnerr { + /* Not enough tokens were provided */ + JSMN_ERROR_NOMEM = -1, + /* Invalid character inside JSON string */ + JSMN_ERROR_INVAL = -2, + /* The string is not a full JSON packet, more bytes expected */ + JSMN_ERROR_PART = -3 +}; + +/** + * JSON token description. + * type type (object, array, string etc.) + * start start position in JSON data string + * end end position in JSON data string + */ +typedef struct jsmntok { + jsmntype_t type; + int start; + int end; + int size; +#ifdef JSMN_PARENT_LINKS + int parent; +#endif +} jsmntok_t; + +/** + * JSON parser. Contains an array of token blocks available. Also stores + * the string being parsed now and current position in that string. + */ +typedef struct jsmn_parser { + unsigned int pos; /* offset in the JSON string */ + unsigned int toknext; /* next token to allocate */ + int toksuper; /* superior token node, e.g. parent object or array */ +} jsmn_parser; + +/** + * Create JSON parser over an array of tokens + */ +JSMN_API void jsmn_init(jsmn_parser *parser); + +/** + * Run JSON parser. It parses a JSON data string into and array of tokens, each + * describing + * a single JSON object. + */ +JSMN_API int jsmn_parse(jsmn_parser *parser, const char *js, const size_t len, + jsmntok_t *tokens, const unsigned int num_tokens); + +#ifndef JSMN_HEADER +/** + * Allocates a fresh unused token from the token pool. + */ +static jsmntok_t *jsmn_alloc_token(jsmn_parser *parser, jsmntok_t *tokens, + const size_t num_tokens) { + jsmntok_t *tok; + if (parser->toknext >= num_tokens) { + return NULL; + } + tok = &tokens[parser->toknext++]; + tok->start = tok->end = -1; + tok->size = 0; +#ifdef JSMN_PARENT_LINKS + tok->parent = -1; +#endif + return tok; +} + +/** + * Fills token type and boundaries. + */ +static void jsmn_fill_token(jsmntok_t *token, const jsmntype_t type, + const int start, const int end) { + token->type = type; + token->start = start; + token->end = end; + token->size = 0; +} + +/** + * Fills next available token with JSON primitive. + */ +static int jsmn_parse_primitive(jsmn_parser *parser, const char *js, + const size_t len, jsmntok_t *tokens, + const size_t num_tokens) { + jsmntok_t *token; + int start; + + start = parser->pos; + + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + switch (js[parser->pos]) { +#ifndef JSMN_STRICT + /* In strict mode primitive must be followed by "," or "}" or "]" */ + case ':': +#endif + case '\t': + case '\r': + case '\n': + case ' ': + case ',': + case ']': + case '}': + goto found; + default: + /* to quiet a warning from gcc*/ + break; + } + if (js[parser->pos] < 32 || js[parser->pos] >= 127) { + parser->pos = start; + return JSMN_ERROR_INVAL; + } + } +#ifdef JSMN_STRICT + /* In strict mode primitive must be followed by a comma/object/array */ + parser->pos = start; + return JSMN_ERROR_PART; +#endif + +found: + if (tokens == NULL) { + parser->pos--; + return 0; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) { + parser->pos = start; + return JSMN_ERROR_NOMEM; + } + jsmn_fill_token(token, JSMN_PRIMITIVE, start, parser->pos); +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + parser->pos--; + return 0; +} + +/** + * Fills next token with JSON string. + */ +static int jsmn_parse_string(jsmn_parser *parser, const char *js, + const size_t len, jsmntok_t *tokens, + const size_t num_tokens) { + jsmntok_t *token; + + int start = parser->pos; + + /* Skip starting quote */ + parser->pos++; + + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + char c = js[parser->pos]; + + /* Quote: end of string */ + if (c == '\"') { + if (tokens == NULL) { + return 0; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) { + parser->pos = start; + return JSMN_ERROR_NOMEM; + } + jsmn_fill_token(token, JSMN_STRING, start + 1, parser->pos); +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + return 0; + } + + /* Backslash: Quoted symbol expected */ + if (c == '\\' && parser->pos + 1 < len) { + int i; + parser->pos++; + switch (js[parser->pos]) { + /* Allowed escaped symbols */ + case '\"': + case '/': + case '\\': + case 'b': + case 'f': + case 'r': + case 'n': + case 't': + break; + /* Allows escaped symbol \uXXXX */ + case 'u': + parser->pos++; + for (i = 0; i < 4 && parser->pos < len && js[parser->pos] != '\0'; + i++) { + /* If it isn't a hex character we have an error */ + if (!((js[parser->pos] >= 48 && js[parser->pos] <= 57) || /* 0-9 */ + (js[parser->pos] >= 65 && js[parser->pos] <= 70) || /* A-F */ + (js[parser->pos] >= 97 && js[parser->pos] <= 102))) { /* a-f */ + parser->pos = start; + return JSMN_ERROR_INVAL; + } + parser->pos++; + } + parser->pos--; + break; + /* Unexpected symbol */ + default: + parser->pos = start; + return JSMN_ERROR_INVAL; + } + } + } + parser->pos = start; + return JSMN_ERROR_PART; +} + +/** + * Parse JSON string and fill tokens. + */ +JSMN_API int jsmn_parse(jsmn_parser *parser, const char *js, const size_t len, + jsmntok_t *tokens, const unsigned int num_tokens) { + int r; + int i; + jsmntok_t *token; + int count = parser->toknext; + + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + char c; + jsmntype_t type; + + c = js[parser->pos]; + switch (c) { + case '{': + case '[': + count++; + if (tokens == NULL) { + break; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) { + return JSMN_ERROR_NOMEM; + } + if (parser->toksuper != -1) { + jsmntok_t *t = &tokens[parser->toksuper]; +#ifdef JSMN_STRICT + /* In strict mode an object or array can't become a key */ + if (t->type == JSMN_OBJECT) { + return JSMN_ERROR_INVAL; + } +#endif + t->size++; +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + } + token->type = (c == '{' ? JSMN_OBJECT : JSMN_ARRAY); + token->start = parser->pos; + parser->toksuper = parser->toknext - 1; + break; + case '}': + case ']': + if (tokens == NULL) { + break; + } + type = (c == '}' ? JSMN_OBJECT : JSMN_ARRAY); +#ifdef JSMN_PARENT_LINKS + if (parser->toknext < 1) { + return JSMN_ERROR_INVAL; + } + token = &tokens[parser->toknext - 1]; + for (;;) { + if (token->start != -1 && token->end == -1) { + if (token->type != type) { + return JSMN_ERROR_INVAL; + } + token->end = parser->pos + 1; + parser->toksuper = token->parent; + break; + } + if (token->parent == -1) { + if (token->type != type || parser->toksuper == -1) { + return JSMN_ERROR_INVAL; + } + break; + } + token = &tokens[token->parent]; + } +#else + for (i = parser->toknext - 1; i >= 0; i--) { + token = &tokens[i]; + if (token->start != -1 && token->end == -1) { + if (token->type != type) { + return JSMN_ERROR_INVAL; + } + parser->toksuper = -1; + token->end = parser->pos + 1; + break; + } + } + /* Error if unmatched closing bracket */ + if (i == -1) { + return JSMN_ERROR_INVAL; + } + for (; i >= 0; i--) { + token = &tokens[i]; + if (token->start != -1 && token->end == -1) { + parser->toksuper = i; + break; + } + } +#endif + break; + case '\"': + r = jsmn_parse_string(parser, js, len, tokens, num_tokens); + if (r < 0) { + return r; + } + count++; + if (parser->toksuper != -1 && tokens != NULL) { + tokens[parser->toksuper].size++; + } + break; + case '\t': + case '\r': + case '\n': + case ' ': + break; + case ':': + parser->toksuper = parser->toknext - 1; + break; + case ',': + if (tokens != NULL && parser->toksuper != -1 && + tokens[parser->toksuper].type != JSMN_ARRAY && + tokens[parser->toksuper].type != JSMN_OBJECT) { +#ifdef JSMN_PARENT_LINKS + parser->toksuper = tokens[parser->toksuper].parent; +#else + for (i = parser->toknext - 1; i >= 0; i--) { + if (tokens[i].type == JSMN_ARRAY || tokens[i].type == JSMN_OBJECT) { + if (tokens[i].start != -1 && tokens[i].end == -1) { + parser->toksuper = i; + break; + } + } + } +#endif + } + break; +#ifdef JSMN_STRICT + /* In strict mode primitives are: numbers and booleans */ + case '-': + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case 't': + case 'f': + case 'n': + /* And they must not be keys of the object */ + if (tokens != NULL && parser->toksuper != -1) { + const jsmntok_t *t = &tokens[parser->toksuper]; + if (t->type == JSMN_OBJECT || + (t->type == JSMN_STRING && t->size != 0)) { + return JSMN_ERROR_INVAL; + } + } +#else + /* In non-strict mode every unquoted value is a primitive */ + default: +#endif + r = jsmn_parse_primitive(parser, js, len, tokens, num_tokens); + if (r < 0) { + return r; + } + count++; + if (parser->toksuper != -1 && tokens != NULL) { + tokens[parser->toksuper].size++; + } + break; + +#ifdef JSMN_STRICT + /* Unexpected char in strict mode */ + default: + return JSMN_ERROR_INVAL; +#endif + } + } + + if (tokens != NULL) { + for (i = parser->toknext - 1; i >= 0; i--) { + /* Unmatched opened object or array */ + if (tokens[i].start != -1 && tokens[i].end == -1) { + return JSMN_ERROR_PART; + } + } + } + + return count; +} + +/** + * Creates a new parser based over a given buffer with an array of tokens + * available. + */ +JSMN_API void jsmn_init(jsmn_parser *parser) { + parser->pos = 0; + parser->toknext = 0; + parser->toksuper = -1; +} + +#endif /* JSMN_HEADER */ + +#ifdef __cplusplus +} +#endif + +#endif /* JSMN_H */ diff --git a/json.c b/json.c @@ -0,0 +1,137 @@ +#include <stdio.h> +#include <string.h> +#include "json.h" +#include "jsmn.h" + +static int tok_streq(const char *js, jsmntok_t *t, const char *s) { + return (t->type == JSMN_STRING && + (int)strlen(s) == t->end - t->start && + strncmp(js + t->start, s, t->end - t->start) == 0); +} + +static void scan(const char *js, jsmntok_t *t, int *i, int ntok) { + if (*i >= ntok) return; + + if (t[*i].type == JSMN_OBJECT) { + int n = t[*i].size; + (*i)++; + + const char *id = NULL, *from = NULL, *subject = NULL, *date = NULL, *body = NULL; + int id_len=0, from_len=0, subject_len=0, date_len=0, body_len=0; + + for (int k = 0; k < n; k++) { + jsmntok_t *key = &t[*i]; (*i)++; + jsmntok_t *val = &t[*i]; + + /* id */ + if (tok_streq(js, key, "id")) { + id = js + val->start; + id_len = val->end - val->start; + (*i)++; + } + /* headers */ + else if (tok_streq(js, key, "headers") && val->type == JSMN_OBJECT) { + int hn = val->size; + (*i)++; + for (int h = 0; h < hn; h++) { + jsmntok_t *hkey = &t[*i]; (*i)++; + jsmntok_t *hval = &t[*i]; + + if (tok_streq(js, hkey, "From")) { + from = js + hval->start; + from_len = hval->end - hval->start; + } else if (tok_streq(js, hkey, "Subject")) { + subject = js + hval->start; + subject_len = hval->end - hval->start; + } else if (tok_streq(js, hkey, "Date")) { + date = js + hval->start; + date_len = hval->end - hval->start; + } + (*i)++; + } + } + /* body */ + else if (tok_streq(js, key, "body") && val->type == JSMN_ARRAY) { + int bn = val->size; + (*i)++; + for (int b = 0; b < bn; b++) { + if (t[*i].type == JSMN_OBJECT) { + int on = t[*i].size; + (*i)++; + const char *ctype = NULL; + int ctype_len = 0; + + for (int x = 0; x < on; x++) { + jsmntok_t *bkey = &t[*i]; (*i)++; + jsmntok_t *bval = &t[*i]; + + if (tok_streq(js, bkey, "content-type")) { + ctype = js + bval->start; + ctype_len = bval->end - bval->start; + } else if (tok_streq(js, bkey, "content")) { + if (ctype && strncmp(ctype, "text/plain", ctype_len) == 0) { + body = js + bval->start; + body_len = bval->end - bval->start; + } + } + (*i)++; + } + } else { + (*i)++; + } + } + } + else { + /* skip value */ + if (val->type == JSMN_OBJECT || val->type == JSMN_ARRAY) { + int skip = val->size * 2; + (*i)++; + for (int s = 0; s < skip; s++) (*i)++; + } else { + (*i)++; + } + } + } + + /* If this object looks like a message, print it */ + if (id && subject && from && date) { + printf("Message-ID: %.*s\n", id_len, id); + printf("From: %.*s\n", from_len, from); + printf("Subject: %.*s\n", subject_len, subject); + printf("Date: %.*s\n", date_len, date); + if (body) { + printf("Body:\n%.*s\n", body_len, body); + } + printf("--------------------------------------------------\n"); + } + return; + } + + /* recurse arrays */ + if (t[*i].type == JSMN_ARRAY) { + int n = t[*i].size; + (*i)++; + for (int k = 0; k < n; k++) { + scan(js, t, i, ntok); + } + return; + } + + (*i)++; +} + +void parse_mail_json(const char *json, int len) { + jsmn_parser p; + jsmn_init(&p); + + jsmntok_t tokens[4096]; + int ntok = jsmn_parse(&p, json, len, tokens, 4096); + if (ntok < 0) { + fprintf(stderr, "json: parse error\n"); + return; + } + + int i = 0; + scan(json, tokens, &i, ntok); +} + diff --git a/json.h b/json.h @@ -0,0 +1,8 @@ +#ifndef JSON_H +#define JSON_H + +/* Parse JSON buffer and print extracted mail fields */ +void parse_mail_json(const char *json, int len); + +#endif + diff --git a/stamail.c b/stamail.c @@ -0,0 +1,30 @@ +#include <stdio.h> +#include <stdlib.h> +#include "json.h" + +int main(void) { + /* Slurp stdin */ + char *buf = NULL; + size_t cap = 0, len = 0; + int c; + + while ((c = getchar()) != EOF) { + if (len + 1 >= cap) { + cap = cap ? cap * 2 : 4096; + buf = realloc(buf, cap); + if (!buf) { + perror("realloc"); + return 1; + } + } + buf[len++] = c; + } + + if (!buf) return 0; + + parse_mail_json(buf, len); + + free(buf); + return 0; +} +