diff options
author | Daniel Borkmann <daniel@iogearbox.net> | 2019-09-17 12:43:21 +0200 |
---|---|---|
committer | Daniel Borkmann <daniel@iogearbox.net> | 2019-09-25 22:39:42 +0200 |
commit | 76ee0b229225193165ac2a6f49a9b9d0a714463f (patch) | |
tree | 893746923f9a508aa291f9a1e7ebdada6612d9ec | |
download | l2md-76ee0b229225193165ac2a6f49a9b9d0a714463f.tar.gz |
l2md: initial import of lore 2 maildir
There it is. See README for more details and setup.
Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
-rw-r--r-- | COPYING | 347 | ||||
-rw-r--r-- | Makefile | 14 | ||||
-rw-r--r-- | README | 93 | ||||
-rw-r--r-- | config.c | 214 | ||||
-rw-r--r-- | env.c | 98 | ||||
-rw-r--r-- | l2md.c | 50 | ||||
-rw-r--r-- | l2md.h | 127 | ||||
-rw-r--r-- | l2md.service | 13 | ||||
-rw-r--r-- | l2mdconfig | 14 | ||||
-rw-r--r-- | mail.c | 44 | ||||
-rw-r--r-- | muttrc | 12 | ||||
-rw-r--r-- | repo.c | 277 | ||||
-rw-r--r-- | utils.c | 254 |
13 files changed, 1557 insertions, 0 deletions
@@ -0,0 +1,347 @@ + Note that the only valid version of the GPL as far as this project is + concerned is _this_ particular version of the license (i.e. v2, not v2.2 + or v3.x or whatever), unless explicitly otherwise stated. + + Daniel Borkmann + +----------------------------------------------------------------------------- + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + <signature of Ty Coon>, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..19b0ce8 --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +CFLAGS = -O2 -Wall -Werror +LDFLAGS = -lgit2 + +l2md: l2md.o config.o env.o utils.o repo.o mail.o + $(CC) -o $@ $^ $(LDFLAGS) + +install: + cp l2md /usr/bin/l2md + +uninstall: + $(RM) /usr/bin/l2md + +clean: + $(RM) *.o l2md @@ -0,0 +1,93 @@ +l2md - lore2maildir +------------------- + +Quick and dirty hack to import lore.kernel.org list archives via git, +export them in maildir format and keep them periodically synced. + +It can then be used in whichever mail client that supports maildir +format, for example, mutt. + +Essentially, it avoids the need to subscribe to any of the lore lists +via mail since all messages are now imported through git transport. + +Together with a smtp client like msmtp (which you may need anyway for +git-send-email), it allows to interact on the mailing lists the usual +way. + +All pretty basic and hacky at this point, patches very welcome. Please +send them to Daniel Borkmann <daniel@iogearbox.net>. + +Build +----- + +Links to -lgit2 which is shipped by pretty much all major distros. + +Fedora: libgit2-devel +Ubuntu: libgit2-dev + +To build, just type: + +$ make +[...] + +After setting up ~/.l2mdconfig (see below), run as: + +$ ./l2md + +To install, just type: + +# make install + +The l2md.service file contains an example systemd service deployment +for letting it run in the background ... + +$ service l2md status +Redirecting to /bin/systemctl status l2md.service +● l2md.service - lore2maildir + Loaded: loaded (/usr/lib/systemd/system/l2md.service; enabled; vendor preset: disabled) + Active: active (running) since Thu 2019-09-19 12:07:17 CEST; 55s ago + Main PID: 31467 (l2md) + Tasks: 1 (limit: 4915) + Memory: 257.9M + CGroup: /system.slice/l2md.service + └─31467 /usr/bin/l2md + +The muttrc file contains an example mutt config for importing the generated +maildir directories. + +Howto +----- + +The repo has a few example configs: l2mdconfig and muttrc. The latter +needs most likey no further explanation, but just to provide an example +on how to import the directories into mutt. + +The l2mdconfig is an example l2md config which needs to be placed under +~/.l2mdconfig . + +$ cat ~/.l2mdconfig +[general] + maildir = ~/.l2md/maildir/common + period = 30 + +# bpf@vger.kernel.org list +[repo bpf] + url = https://lore.kernel.org/bpf/0 + maildir = ~/.l2md/maildir/bpf + +# netdev@vger.kernel.org list +[repo netdev] + url = https://lore.kernel.org/netdev/1 + url = https://lore.kernel.org/netdev/0 + initial_import = 1000 + +The general section contains a sync period in seconds where l2md refetches +all the git repos and looks for new messages to export into the configured +maildirs. The maildir under general is a path to a shared maildir where +l2md exports new mails into. This can also be specified on a per repository +basis. + +The repo sections with subsequent name define a repository (duh!) with +one or more git urls to lore and optional maildir export path as mentioned. +If initial_import is set to >0, then it will only import first x mails upon +initial repository creation instead of the entire archive. diff --git a/config.c b/config.c new file mode 100644 index 0000000..eb6d356 --- /dev/null +++ b/config.c @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Copyright (C) 2019 Daniel Borkmann <daniel@iogearbox.net> */ + +#include <stdio.h> +#include <stdlib.h> +#include <errno.h> +#include <string.h> +#include <wordexp.h> +#include <stdbool.h> + +#include "l2md.h" + +enum { + STATE_NONE = 0, + STATE_GENERAL, + STATE_REPO, +}; + +static void config_dump(struct config *cfg) +{ + struct config_repo *repo; + struct config_url *url; + uint32_t i, j; + + if (!verbose_enabled) + return; + + verbose("general.maildir = %s\n", cfg->general.maildir); + verbose("general.period = %u\n", cfg->general.period); + + repo_for_each(cfg, repo, i) { + verbose("repos.%s.maildir = %s\n", repo->name, repo->maildir); + verbose("repos.%s.initial_import = %u\n", repo->name, repo->initial_import); + url_for_each(repo, url, j) { + verbose("repos.%s.url = %s\n", repo->name, url->path); + verbose("repos.%s.oid_maildir = %s\n", + repo->name, url->oid_known ? url->oid_maildir : + "[unknown]"); + } + } +} + +static void config_probe_oids(struct config *cfg) +{ + struct config_repo *repo; + struct config_url *url; + char path[PATH_MAX]; + uint32_t i, j; + int ret; + + repo_for_each(cfg, repo, i) { + url_for_each(repo, url, j) { + repo_local_oid(cfg, repo, url, path, sizeof(path)); + ret = xread_file(path, url->oid_maildir, + sizeof(url->oid_maildir) - 1, false); + if (!ret) + url->oid_known = true; + } + } +} + +static void config_set_maildir(struct config *cfg, const char *dir, bool root) +{ + struct config_repo *repo = repo_last(cfg); + char *maildir = root ? cfg->general.maildir : repo->maildir; + wordexp_t p; + + wordexp(dir, &p, 0); + dir = p.we_wordv[0]; + strlcpy(maildir, dir, sizeof(repo->maildir)); + wordfree(&p); +} + +static void config_set_initial_import(struct config *cfg, uint32_t limit) +{ + struct config_repo *repo = repo_last(cfg); + + repo->initial_import = limit; + if (repo->initial_import > 0) + repo->limit = true; +} + +static void config_new_url(struct config *cfg, const char *git_url) +{ + struct config_repo *repo = repo_last(cfg); + struct config_url *url; + + repo->urls_num++; + repo->urls = xrealloc(repo->urls, sizeof(*repo->urls) * + repo->urls_num); + url = url_last(repo); + memset(url, 0, sizeof(*url)); + strlcpy(url->path, git_url, sizeof(url->path)); +} + +static void config_new_repo(struct config *cfg, const char *name) +{ + struct config_repo *repo; + + cfg->repos_num++; + cfg->repos = xrealloc(cfg->repos, sizeof(*cfg->repos) * + cfg->repos_num); + repo = repo_last(cfg); + memset(repo, 0, sizeof(*repo)); + config_set_maildir(cfg, cfg->general.maildir, false); + strlcpy(repo->name, name, sizeof(repo->name)); +} + +static void config_set_defaults(struct config *cfg, const char *homedir) +{ + char path[PATH_MAX]; + + cfg->general.period = 60; + + slprintf(path, sizeof(path), "%s/.l2md/", homedir); + strlcpy(cfg->general.base, path, sizeof(cfg->general.base)); + + slprintf(path, sizeof(path), "%s/.l2md/maildir", homedir); + strlcpy(cfg->general.maildir, path, sizeof(cfg->general.maildir)); +} + +void config_uninit(struct config *cfg) +{ + struct config_repo *repo; + uint32_t i; + + repo_for_each(cfg, repo, i) + xfree(repo->urls); + xfree(cfg->repos); + xfree(cfg); +} + +struct config *config_init(int argc, char **argv) +{ + const char *homedir = getenv("HOME"); + char buff[1024], tmp[1024]; + char path[PATH_MAX]; + int state = STATE_NONE; + struct config *cfg; + FILE *fp; + + if (argc > 1) { + if (argc == 2 && !strncmp(argv[1], "--verbose", + sizeof("--verbose"))) + verbose_enabled = true; + else + panic("Usage: %s [--verbose]\n", argv[0]); + } + + if (!homedir) + panic("Cannot retrieve $HOME from env!\n"); + + slprintf(path, sizeof(path), "%s/.l2mdconfig", homedir); + fp = fopen(path, "r"); + if (!fp) + panic("Cannot open config %s: %s\n", tmp, strerror(errno)); + + cfg = xzmalloc(sizeof(*cfg)); + config_set_defaults(cfg, homedir); + + while (fgets(buff, sizeof(buff), fp)) { + uint32_t val; + + if (buff[0] == '#' || buff[0] == '\n') + continue; + + switch (state) { + case STATE_NONE: + state_next: + if (!strcmp(buff, "[general]\n")) { + state = STATE_GENERAL; + continue; + } else if (sscanf(buff, "[repo %[a-z0-9-]]\n", + tmp) == 1) { + state = STATE_REPO; + config_new_repo(cfg, tmp); + continue; + } else { + panic("Cannot parse: '%s'\n", buff); + } + break; + + case STATE_GENERAL: + if (sscanf(buff, "\tperiod = %u", &val) == 1) + cfg->general.period = val; + else if (sscanf(buff, "\tmaildir = %s", tmp) == 1) + config_set_maildir(cfg, tmp, true); + else + goto state_next; + break; + + case STATE_REPO: + if (sscanf(buff, "\turl = %s", tmp) == 1) + config_new_url(cfg, tmp); + else if (sscanf(buff, "\tmaildir = %s", tmp) == 1) + config_set_maildir(cfg, tmp, false); + else if (sscanf(buff, "\tinitial_import = %u", &val) == 1) + config_set_initial_import(cfg, val); + else + goto state_next; + break; + + default: + panic("Invalid parser state: %d\n", state); + }; + } + + fclose(fp); + + config_probe_oids(cfg); + config_dump(cfg); + + return cfg; +} @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Copyright (C) 2019 Daniel Borkmann <daniel@iogearbox.net> */ + +#include <stdlib.h> +#include <string.h> +#include <libgen.h> +#include <unistd.h> +#include <fcntl.h> + +#include <sys/stat.h> +#include <sys/types.h> + +#include "l2md.h" + +static void bootstrap_base(struct config *cfg) +{ + xmkdir1(cfg->general.base); + xmkdir2(cfg->general.base, REPOS); + xmkdir2(cfg->general.base, OIDS); +} + +static void bootstrap_repos(struct config *cfg) +{ + struct config_repo *repo; + struct config_url *url; + struct stat sb = {}; + char path[PATH_MAX]; + uint32_t i, j; + int ret; + + verbose("Initial repository check.\n"); + + repo_for_each(cfg, repo, i) { + slprintf(path, sizeof(path), "%s/%s", REPOS, repo->name); + xmkdir2(cfg->general.base, path); + + slprintf(path, sizeof(path), "%s/%s", OIDS, repo->name); + xmkdir2(cfg->general.base, path); + + url_for_each(repo, url, j) { + repo_local_path(cfg, repo, url, path, sizeof(path)); + ret = stat(path, &sb); + if (ret) + repo_clone(repo, url, path); + else + repo_pull(repo, url, path); + } + } + + verbose("Initial repository check completed.\n"); +} + +static void bootstrap_mail(struct config *cfg) +{ + struct config_repo *repo; + uint32_t i; + + repo_for_each(cfg, repo, i) { + xmkdir1_with_subdirs(repo->maildir); + + xmkdir2(repo->maildir, "cur"); + xmkdir2(repo->maildir, "tmp"); + xmkdir2(repo->maildir, "new"); + } +} + +void bootstrap_env(struct config *cfg) +{ + bootstrap_base(cfg); + bootstrap_mail(cfg); + bootstrap_repos(cfg); + + verbose("Bootstrap done.\n"); +} + +void sync_done(struct config *cfg) +{ + verbose("Sync done. Sleeping %us.\n", cfg->general.period); + + sleep(cfg->general.period); +} + +void sync_env(struct config *cfg) +{ + struct config_repo *repo; + struct config_url *url; + char path[PATH_MAX]; + uint32_t i, j; + + verbose("Resyncing repositories.\n"); + + repo_for_each(cfg, repo, i) { + url_for_each(repo, url, j) { + repo_local_path(cfg, repo, url, path, sizeof(path)); + repo_pull(repo, url, path); + } + } +} @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Copyright (C) 2019 Daniel Borkmann <daniel@iogearbox.net> */ + +#include <stdio.h> +#include <stdlib.h> +#include <signal.h> +#include <unistd.h> +#include <git2.h> + +#include "l2md.h" + +pid_t own_pid; + +static volatile sig_atomic_t sigint; + +static void signal_handler(int number) +{ + if (number == SIGINT) + sigint = 1; +} + +static __attribute__((constructor)) void main_init(void) +{ + git_libgit2_init(); + own_pid = getpid(); +} + +static __attribute__((destructor)) void main_exit(void) +{ + git_libgit2_shutdown(); +} + +int main(int argc, char **argv) +{ + struct config *cfg = config_init(argc, argv); + + bootstrap_env(cfg); + signal(SIGINT, signal_handler); + + while (!sigint) { + sync_mail(cfg); + sync_done(cfg); + if (sigint) + break; + sync_env(cfg); + } + + config_uninit(cfg); + return 0; +} @@ -0,0 +1,127 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* Copyright (C) 2019 Daniel Borkmann <daniel@iogearbox.net> */ + +#ifndef L2MD_H +#define L2MD_H + +#include <stdio.h> +#include <stdint.h> +#include <stdbool.h> +#include <limits.h> +#include <git2.h> +#include <unistd.h> + +#include <sys/types.h> +#include <sys/time.h> + +/* libgit2 backwards compat crap. */ +#ifndef GIT_OBJECT_BLOB +# define GIT_OLD_VERSION 1 +#endif +#ifdef GIT_OLD_VERSION +# define GIT_OBJECT_BLOB GIT_OBJ_BLOB +# define GIT_OBJECT_COMMIT GIT_OBJ_COMMIT +# define git_error_last giterr_last +#endif + +#ifndef __check_format_printf +# define __check_format_printf(pos_fmtstr, pos_fmtargs) \ + __attribute__ ((format (printf, (pos_fmtstr), (pos_fmtargs)))) +#endif + +#define REPOS "repos" +#define OIDS "oids" + +#define MAIL "m" + +struct config_general { + char maildir[PATH_MAX]; + char base[PATH_MAX]; + uint32_t period; +}; + +struct config_url { + char path[PATH_MAX]; + char oid_maildir[GIT_OID_HEXSZ + 1]; + bool oid_known; +}; + +struct config_repo { + char name[128]; + char maildir[PATH_MAX]; + git_repository *git; + struct config_url *urls; + uint32_t urls_num; + uint32_t initial_import; + bool limit; +}; + +struct config { + struct config_general general; + struct config_repo *repos; + uint32_t repos_num; +}; + +#define repo_for_each(cfg, repo, i) \ + for (repo = &cfg->repos[(i = 0)]; i < cfg->repos_num; \ + repo = &cfg->repos[++i]) + +#define url_for_each(repo, url, i) \ + for (url = &repo->urls[(i = 0)]; i < repo->urls_num; \ + url = &repo->urls[++i]) + +#define repo_last(cfg) \ + &cfg->repos[cfg->repos_num - 1] + +#define url_last(repo) \ + &repo->urls[repo->urls_num - 1] + +extern pid_t own_pid; +extern bool verbose_enabled; + +struct config *config_init(int argc, char **argv); +void config_uninit(struct config *cfg); + +void bootstrap_env(struct config *cfg); + +void sync_env(struct config *cfg); +void sync_mail(struct config *cfg); +void sync_done(struct config *cfg); + +void repo_clone(struct config_repo *repo, struct config_url *url, const char *target); +void repo_pull(struct config_repo *repo, struct config_url *url, const char *target); +void repo_walk_files(struct config *cfg, struct config_repo *repo, uint32_t which, + const char *path, const char *oid_last, char *oid_done, + void (*repo_walker)(struct config *cfg, struct config_repo *repo, + uint32_t which, const char *oid, + const void *raw, size_t len)); + +void repo_local_path(struct config *cfg, struct config_repo *repo, + struct config_url *url, char *out, size_t len); +void repo_local_oid(struct config *cfg, struct config_repo *repo, + struct config_url *url, char *out, size_t len); + +void *xmalloc(size_t size); +void *xzmalloc(size_t size); +void *xrealloc(void *old, size_t size); +void xfree(void *ptr); + +void xmkdir1(const char *path); +void xmkdir2(const char *base, const char *name); +void xmkdir1_with_subdirs(const char *path); + +int xread_file(const char *file, char *to, size_t len, bool fatal); +int xwrite_file(const char *file, const char *to, size_t len, bool fatal); + +int timeval_sub(struct timeval *res, struct timeval *x, + struct timeval *y); + +size_t strlcpy(char *dest, const char *src, size_t size); +int slprintf(char *dst, size_t size, const char *fmt, ...); + +void verbose(const char *format, ...) __check_format_printf(1, 2); +void panic(const char *format, ...) __check_format_printf(1, 2); +void warn(const char *format, ...) __check_format_printf(1, 2); +void die(void); + +#endif /* L2MD_H */ diff --git a/l2md.service b/l2md.service new file mode 100644 index 0000000..917cac2 --- /dev/null +++ b/l2md.service @@ -0,0 +1,13 @@ +[Unit] +Description=lore2maildir +After=network-online.target + +[Service] +Type=simple +User=darkstar +WorkingDirectory=/home/darkstar/ +ExecStart=/usr/bin/l2md +Restart=on-failure + +[Install] +WantedBy=multi-user.target diff --git a/l2mdconfig b/l2mdconfig new file mode 100644 index 0000000..8222324 --- /dev/null +++ b/l2mdconfig @@ -0,0 +1,14 @@ +[general] + maildir = ~/.l2md/maildir/common + period = 30 + +# bpf@vger.kernel.org list +[repo bpf] + url = https://lore.kernel.org/bpf/0 + maildir = ~/.l2md/maildir/bpf + +# netdev@vger.kernel.org list +[repo netdev] + url = https://lore.kernel.org/netdev/1 + url = https://lore.kernel.org/netdev/0 + initial_import = 1000 @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Copyright (C) 2019 Daniel Borkmann <daniel@iogearbox.net> */ + +#include <unistd.h> + +#include <sys/time.h> +#include <sys/types.h> + +#include "l2md.h" + +static void repo_walker(struct config *cfg, struct config_repo *repo, uint32_t which, + const char *oid, const void *raw, size_t len) +{ + char dst[PATH_MAX]; + + slprintf(dst, sizeof(dst), "%s/new/0.%06u.%s-%u-%s", + repo->maildir, own_pid, repo->name, which, oid); + + xwrite_file(dst, raw, len, true); +} + +void sync_mail(struct config *cfg) +{ + struct config_repo *repo; + struct config_url *url; + char path[PATH_MAX]; + uint32_t i, j; + + verbose("Resyncing maildirs.\n"); + + repo_for_each(cfg, repo, i) { + url_for_each(repo, url, j) { + repo_local_path(cfg, repo, url, path, sizeof(path)); + repo_walk_files(cfg, repo, j, path, + url->oid_known ? url->oid_maildir : NULL, + url->oid_maildir, repo_walker); + + repo_local_oid(cfg, repo, url, path, sizeof(path)); + xwrite_file(path, url->oid_maildir, + sizeof(url->oid_maildir) - 1, true); + url->oid_known = true; + } + } +} @@ -0,0 +1,12 @@ +# Setting up maildir and pointing l2md bpf dir to it: +set mbox_type = Maildir +set folder = "~/.l2md/maildir/bpf/" +set header_cache = "~/.cache/mutt" +set spoolfile = +/ + +# Sorting by discussion threads: +set sort = "threads" +set strict_threads = "yes" +set sort_browser = "reverse-date" +set sort_aux = "reverse-last-date-received" +unset collapse_unread @@ -0,0 +1,277 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Copyright (C) 2019 Daniel Borkmann <daniel@iogearbox.net> */ + +#include <stdio.h> +#include <string.h> +#include <libgen.h> + +#include "l2md.h" + +static int progress_sideband(const char *msg, int len, void *private_data) +{ + verbose("Remote: %.*s", len, msg); + fflush(stdout); + return 0; +} + +static void progress_checkout(const char *path, size_t curr, size_t total, + void *private_data) +{ + verbose("Checkout: %zu/%zu\r", curr, total); + fflush(stdout); +} + +static void setup_fetch_opts(git_fetch_options *opts) +{ + opts->callbacks.sideband_progress = progress_sideband; +} + +static void setup_checkout_opts(git_checkout_options *opts) +{ + opts->checkout_strategy = GIT_CHECKOUT_SAFE; + opts->progress_cb = progress_checkout; +} + +static void setup_clone_opts(git_clone_options *opts) +{ + setup_checkout_opts(&opts->checkout_opts); + setup_fetch_opts(&opts->fetch_opts); +} + +static void panic_git(const char *subject) +{ + const git_error *err = git_error_last(); + const char *err_str = err ? err->message : "unknown error"; + + panic("%s: %s\n", subject, err_str); +} + +static void warn_git(const char *subject) +{ + const git_error *err = git_error_last(); + const char *err_str = err ? err->message : "unknown error"; + + warn("%s: %s\n", subject, err_str); +} + +void repo_clone(struct config_repo *repo, struct config_url *url, + const char *target) +{ + git_clone_options clone_opts = GIT_CLONE_OPTIONS_INIT; + int ret; + + setup_clone_opts(&clone_opts); + + verbose("Cloning: %s\n", url->path); + + ret = git_clone(&repo->git, url->path, target, &clone_opts); + if (ret) + panic_git("Cannot clone git repo"); + + git_repository_free(repo->git); +} + +static int fetch_head(const char *ref_name, const char *remote_url, + const git_oid *oid, unsigned int merge, + void *private_data) +{ + char out[GIT_OID_HEXSZ + 1]; + + if (merge) { + verbose("Merging: %s commit %s\n", ref_name, + git_oid_tostr(out, sizeof(out), oid)); + git_oid_cpy(private_data, oid); + } + + return 0; +} + +void repo_pull(struct config_repo *repo, struct config_url *url, + const char *target) +{ + git_checkout_options checkout_opts = GIT_CHECKOUT_OPTIONS_INIT; + git_fetch_options fetch_opts = GIT_FETCH_OPTIONS_INIT; + git_reference *head_cur, *head_new; + git_annotated_commit *head; + git_oid oid_to_merge; + git_object *head_obj; + git_remote *remote; + int ret; + + ret = git_repository_open(&repo->git, target); + if (ret) + panic_git("Cannot open git repo"); + + ret = git_remote_lookup(&remote, repo->git, "origin"); + if (ret) + panic_git("Cannot look up origin"); + + setup_fetch_opts(&fetch_opts); + + verbose("Fetching: %s\n", url->path); + + ret = git_remote_fetch(remote, NULL, &fetch_opts, + NULL); + if (ret) { + warn_git("Cannot fetch remote"); + goto out; + } + + git_repository_fetchhead_foreach(repo->git, fetch_head, + &oid_to_merge); + + ret = git_annotated_commit_lookup(&head, repo->git, + &oid_to_merge); + if (ret) + panic_git("Cannot lookup head"); + + ret = git_repository_head(&head_cur, repo->git); + if (ret) + panic_git("Cannot lookup current head"); + + ret = git_object_lookup(&head_obj, repo->git, &oid_to_merge, + GIT_OBJECT_COMMIT); + if (ret) + panic_git("Cannot lookup HEAD object"); + + setup_checkout_opts(&checkout_opts); + + ret = git_checkout_tree(repo->git, head_obj, &checkout_opts); + if (ret) + panic_git("Cannot checkout tree"); + + ret = git_reference_set_target(&head_new, head_cur, &oid_to_merge, + NULL); + if (ret) + panic_git("Cannot repoint HEAD"); + + git_repository_state_cleanup(repo->git); + + git_annotated_commit_free(head); + + git_reference_free(head_cur); + git_reference_free(head_new); + + git_object_free(head_obj); +out: + git_remote_free(remote); + git_repository_free(repo->git); +} + +static void __repo_local_get(struct config *cfg, struct config_repo *repo, + struct config_url *url, const char *which, + char *out, size_t len) +{ + char tmp[PATH_MAX]; + + strcpy(tmp, url->path); + slprintf(out, len, "%s/%s/%s/%s", cfg->general.base, which, + repo->name, basename(tmp)); +} + +void repo_local_path(struct config *cfg, struct config_repo *repo, + struct config_url *url, char *out, size_t len) +{ + __repo_local_get(cfg, repo, url, REPOS, out, len); +} + +void repo_local_oid(struct config *cfg, struct config_repo *repo, + struct config_url *url, char *out, size_t len) +{ + __repo_local_get(cfg, repo, url, OIDS, out, len); +} + +void repo_walk_files(struct config *cfg, struct config_repo *repo, uint32_t which, + const char *path, const char *oid_last, char *oid_done, + void (*repo_walker)(struct config *cfg, struct config_repo *repo, + uint32_t which, const char *oid, + const void *raw, size_t len)) +{ + char oid_curr[GIT_OID_HEXSZ + 1]; + struct timeval start, end, res; + git_oid coid, loid, foid; + bool have_first = false; + const git_blob *blob; + char spec[PATH_MAX]; + git_revwalk *walker; + git_commit *commit; + git_object *object; + uint32_t count = 0; + int ret; + + ret = git_repository_open(&repo->git, path); + if (ret) + panic_git("Cannot open git repo"); + + ret = git_revwalk_new(&walker, repo->git); + if (ret) + panic_git("Cannot create ref walker"); + + git_revwalk_sorting(walker, GIT_SORT_TIME); + + ret = git_revwalk_push_head(walker); + if (ret) + panic_git("Cannot push head onto ref walker"); + if (oid_last) { + ret = git_oid_fromstrn(&loid, oid_last, GIT_OID_HEXSZ); + if (ret) + panic_git("Cannot convert to git oid"); + } + + gettimeofday(&start, NULL); + + while (!git_revwalk_next(&coid, walker)) { + ret = git_commit_lookup(&commit, repo->git, &coid); + if (ret) + panic_git("Cannot look up commit"); + + if (!have_first) { + git_oid_cpy(&foid, &coid); + have_first = true; + } + + if (oid_last && !git_oid_cmp(&loid, &coid)) { + git_commit_free(commit); + break; + } + + if (!oid_last && repo->limit) { + if (repo->initial_import == 0) { + git_commit_free(commit); + break; + } + repo->initial_import--; + } + + git_oid_tostr(oid_curr, sizeof(oid_curr), &coid); + slprintf(spec, sizeof(spec), "%s:%s", oid_curr, MAIL); + + ret = git_revparse_single(&object, repo->git, spec); + if (ret || git_object_type(object) != GIT_OBJECT_BLOB) + panic_git("Cannot revparse object"); + + blob = (const git_blob *)object; + repo_walker(cfg, repo, which, oid_curr, git_blob_rawcontent(blob), + (size_t)git_blob_rawsize(blob)); + + git_object_free(object); + git_commit_free(commit); + + count++; + } + + gettimeofday(&end, NULL); + timeval_sub(&res, &end, &start); + + if (have_first) + git_oid_tostr(oid_done, GIT_OID_HEXSZ + 1, &foid); + if (have_first && count) { + git_oid_tostr(oid_curr, sizeof(oid_curr), &loid); + printf("Processed %u new mail(s) for %s. %s: %s -> %s in %ld.%02lds.\n", + count, repo->name, repo->urls[which].path, oid_last ? oid_curr : + "[none]", oid_done, res.tv_sec, res.tv_usec); + } + + git_revwalk_free(walker); + git_repository_free(repo->git); +} @@ -0,0 +1,254 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Copyright (C) 2019 Daniel Borkmann <daniel@iogearbox.net> */ + +#include <stdio.h> +#include <stdlib.h> +#include <stdarg.h> +#include <string.h> +#include <errno.h> +#include <unistd.h> +#include <fcntl.h> + +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/syscall.h> + +#include "l2md.h" + +bool verbose_enabled; + +void *xmalloc(size_t size) +{ + void *ptr; + + if (!size) + panic("xmalloc: zero size\n"); + ptr = malloc(size); + if (ptr == NULL) + panic("xmalloc: out of memory (allocating %zu bytes)\n", size); + return ptr; +} + +void *xzmalloc(size_t size) +{ + void *ptr = xmalloc(size); + + memset(ptr, 0, size); + return ptr; +} + +void *xrealloc(void *old, size_t size) +{ + void *new; + + if (size == 0) + panic("xrealloc: zero size\n"); + new = realloc(old, size); + if (new == NULL) + panic("xrealloc: out of memory (allocating %zu bytes)\n", size); + return new; +} + +void xfree(void *ptr) +{ + free(ptr); +} + +void xmkdir1(const char *path) +{ + struct stat sb = {}; + int ret; + + ret = stat(path, &sb); + if (ret) { + ret = mkdir(path, S_IRWXU); + if (ret) + panic("mkdir %s failed: %s\n", path, strerror(errno)); + } +} + +void xmkdir1_with_subdirs(const char *path) +{ + char tmp[PATH_MAX], *ptr; + size_t len; + + slprintf(tmp, sizeof(tmp),"%s", path); + len = strlen(tmp); + if (tmp[len - 1] == '/') + tmp[len - 1] = 0; + for (ptr = tmp + 1; *ptr; ptr++) { + if (*ptr == '/') { + *ptr = 0; + xmkdir1(tmp); + *ptr = '/'; + } + } + xmkdir1(tmp); +} + +void xmkdir2(const char *base, const char *name) +{ + char path[PATH_MAX]; + + slprintf(path, sizeof(path), "%s/%s", base, name); + xmkdir1(path); +} + +int xread_file(const char *file, char *to, size_t len, bool fatal) +{ + loff_t ret; + int fd; + + fd = open(file, O_RDONLY); + if (fd < 0) { + if (fatal) + panic("Cannot open %s: %s", file, strerror(errno)); + else + return fd; + } + + do { + ret = read(fd, to, len); + if (ret < 0) { + if (fatal) { + panic("Cannot read file %s: %s", file, + strerror(errno)); + } else { + close(fd); + return -1; + } + } + to += ret; + len -= ret; + } while (len > 0); + + close(fd); + return 0; +} + +int xwrite_file(const char *file, const char *to, size_t len, bool fatal) +{ + loff_t ret; + int fd; + + fd = open(file, O_CREAT | O_WRONLY | O_TRUNC, 0600); + if (fd < 0) { + if (fatal) + panic("Cannot open %s: %s", file, strerror(errno)); + else + return fd; + } + + do { + ret = write(fd, to, len); + if (ret < 0) { + if (fatal) { + panic("Cannot write file %s: %s", file, + strerror(errno)); + } else { + close(fd); + return -1; + } + } + to += ret; + len -= ret; + } while (len > 0); + + close(fd); + return 0; +} + +int timeval_sub(struct timeval *res, struct timeval *x, + struct timeval *y) +{ + if (x->tv_usec < y->tv_usec) { + int nsec = (y->tv_usec - x->tv_usec) / 1000000 + 1; + + y->tv_usec -= 1000000 * nsec; + y->tv_sec += nsec; + } + + if (x->tv_usec - y->tv_usec > 1000000) { + int nsec = (x->tv_usec - y->tv_usec) / 1000000; + + y->tv_usec += 1000000 * nsec; + y->tv_sec -= nsec; + } + + res->tv_sec = x->tv_sec - y->tv_sec; + res->tv_usec = x->tv_usec - y->tv_usec; + + return x->tv_sec < y->tv_sec; +} + +size_t strlcpy(char *dest, const char *src, size_t size) +{ + size_t ret = strlen(src); + + if (size) { + size_t len = (ret >= size) ? size - 1 : ret; + + memcpy(dest, src, len); + dest[len] = '\0'; + } + + return ret; +} + +static inline int vslprintf(char *dst, size_t size, const char *fmt, va_list ap) +{ + int ret; + + ret = vsnprintf(dst, size, fmt, ap); + dst[size - 1] = '\0'; + + return ret; +} + +int slprintf(char *dst, size_t size, const char *fmt, ...) +{ + va_list ap; + int ret; + + va_start(ap, fmt); + ret = vslprintf(dst, size, fmt, ap); + va_end(ap); + + return ret; +} + +void die(void) +{ + exit(EXIT_FAILURE); +} + +void panic(const char *format, ...) +{ + va_list vl; + + va_start(vl, format); + vfprintf(stderr, format, vl); + va_end(vl); + + die(); +} + +void warn(const char *format, ...) +{ + va_list vl; + + va_start(vl, format); + vfprintf(stderr, format, vl); + va_end(vl); +} + +void verbose(const char *format, ...) +{ + va_list vl; + + if (verbose_enabled) { + va_start(vl, format); + vfprintf(stdout, format, vl); + va_end(vl); + } +} |