Compare commits

..

1 commit

Author SHA1 Message Date
a24e8b43d4 Add a roadmap draft 2022-04-07 11:38:47 +02:00
127 changed files with 8196 additions and 24365 deletions

49
.drone.yml Normal file
View file

@ -0,0 +1,49 @@
---
# see https://docs.drone.io/pipeline/configuration/
kind: pipeline
type: docker
name: build
steps:
- name: submodules
image: alpine/git
commands:
- git submodule update --init --recursive
- cp -rv garage/doc/book content/documentation
- name: build-css
image: node
commands:
- npm install
- npx tailwindcss -i ./src/input.css -o ./static/style.css --minify
- name: build-zola
image: ghcr.io/getzola/zola:v0.15.3
entrypoint: [ "/bin/zola" ]
command: [ "build", "-u", "https://garagehq.deuxfleurs.fr" ]
- name: upload
image: plugins/s3
settings:
bucket: garagehq.deuxfleurs.fr
access_key:
from_secret: aws_access_key_id
secret_key:
from_secret: aws_secret_access_key
source: public/**/*
strip_prefix: public/
target: /
path_style: true
endpoint: https://garage.deuxfleurs.fr
region: garage
when:
branch:
- master
event:
exclude:
- pull_request
---
kind: signature
hmac: 8420ca485ee30f847d4f7f3bf8a40eedb5b4697c91ec1e7005366cdb30282c59
...

2
.gitignore vendored
View file

@ -1,4 +1,4 @@
node_modules
public
content/documentation
static/style.css
static/api

3
.gitmodules vendored Normal file
View file

@ -0,0 +1,3 @@
[submodule "garage"]
path = garage
url = https://git.deuxfleurs.fr/Deuxfleurs/garage.git

View file

@ -1,28 +1,46 @@
# Aerogramme Website
# Garage Website
## Install
¡ Work in progress (almost done) !
```
yarn install
```
---
## Dev
## Setup
```
yarn start
zola serve
```
- Install Zola with `pacman -S zola`
- Clone this repo
- Run `npm install` to get the dev dependencies
- Run `zola build` to get the public directory
- Run `npm start` to compile styles and scripts
- Run `zola serve`
## Build
```
yarn build
zola build
yarn build
```
CSS : `28.4 kB`
## Deploy
JS : `6.8 kB (app)` + `1.2 MB (search)`*
```
aws s3 sync --delete ./public s3://aerogramme.deuxfleurs.fr/
```
*<em>The search index in loaded only when the user opens the search modal</em>
Images + Icons : `1.1 MB`
## Fonctions & utilities
JavaScript can be disabled and the website will still run nicely.
It only brings QoL advantages for the user.
The function is [x] if it still runs <u>without</u> JavaScript enabled.
- [x] Responsive main navigation menu (toggle)
- [x] Documentation : user can deploy or reploy ToC submenus
- [ ] Documentation : deploy only the current ToC submenu after a page change
- [ ] Documentation : sidebar focus effect on current section anchor when user scrolls
- [ ] Documentation : ToC that follows the user's scroll
- [ ] Global search
## Screenshots
<a target="_blank" href="https://git.deuxfleurs.fr/sptl/garage_website/raw/branch/master/static/screenshots/screenshot-480.png">480px</a>
<a target="_blank" href="https://git.deuxfleurs.fr/sptl/garage_website/raw/branch/master/static/screenshots/screenshot-768.png">768px</a>
<a target="_blank" href="https://git.deuxfleurs.fr/sptl/garage_website/raw/branch/master/static/screenshots/screenshot-1280.png">1280px</a>

View file

@ -1,6 +1,6 @@
base_url = "https://aerogramme.deuxfleurs.fr"
title = "Aerogramme"
description = "A robust email server"
base_url = "https://garagehq.deuxfleurs.fr"
title = "Garage"
description = "An open-source distributed storage service you can self-host to fullfill many needs"
default_language = "en"
output_dir = "public"
compile_sass = true
@ -40,8 +40,8 @@ include_path = false
include_content = true
[extra]
katex.enabled = true
katex.auto_render = true
katex.enabled = false
katex.auto_render = false
chart.enabled = false
mermaid.enabled = true
galleria.enabled = false
@ -54,19 +54,22 @@ navbar_items = [
]
[extra.favicon]
favicon_svg = "/logo/aerogramme-blue-sq.svg"
favicon_16x16 = "/icons/favicon-16x16.png"
favicon_32x32 = "/icons/favicon-32x32.png"
apple_touch_icon = "/icons/apple-touch-icon.png"
webmanifest = "/icons/site.webmanifest"
[extra.organization]
name = "Aerogramme"
description = "A robust email server"
logo = "/logo/aerogramme-blue-hz.svg"
logo_simple = "/logo/aeogramme-blue-sq.svg"
logo_horizontal = "/logo/aerogramme-blue-sq.svg"
name = "Garage"
description = "An open-source distributed storage service you can self-host to fullfill many needs"
logo = "/images/garage-logo.svg"
logo_simple = "/images/garage-logo-simple.svg"
logo_horizontal = "/images/garage-logo-horizontal.svg"
[extra.author]
name = "Aerogramme"
avatar = "/logo/aerogramme-blue-sq.svg"
name = "Garage"
avatar = "/images/garage-logo.svg"
[extra.social]
git = "https://git.deuxfleurs.fr/Deuxfleurs/aerogramme"
git = "https://git.deuxfleurs.fr/Deuxfleurs/garage"
email = "garagehq@deuxfleurs.fr"

View file

@ -0,0 +1,52 @@
+++
title="Garage will be at FOSDEM'22"
date=2022-02-02
+++
*FOSDEM is an international meeting about Free Software, organized from Brussels.
On next Sunday, Febuary 6th, 2022, we will be there to present Garage.*
<!-- more -->
---
In 2000, a belgian free software activist going by the name of Raphael Baudin
set out to create a small event for free software developpers in Brussels.
This event quickly became the "Free and Open Source Developers' European Meeting",
shorthand FOSDEM. 22 years later, FOSDEM is a major event for free software developpers
around the world. And for this year, we have the immense pleasure of announcing
that the Deuxfleurs association will be there to present Garage.
The event is usually hosted by the Université Libre de Bruxelles (ULB) and welcomes
around 5000 people. But due to COVID, the event has been taking place online
in the last few years. Nothing too unfamiliar to us, as the organization is using
the same tools as we are: a combination of Jitsi and Matrix.
We are of course extremely honored that our presentation was accepted.
If technical details are your thing, we invite you to come share this event with us.
In all cases, the event will be recorded and available as a VOD (Video On Demand)
afterwards. Concerning the details of the organization:
**When?** On Sunday, Febuary 6th, 2022, from 10:30 AM to 11:00 AM CET.
**What for?** Introducing the Garage storage platform.
**By whom?** The presentation will be made by Alex,
other developpers will be present to answer questions.
**For who?** The presentation is targetted to a technical audience that is knowledgable in software developpement or systems administration.
**Price:** FOSDEM'22 is an entirely free event.
**Where?** Online, in the Software Defined Storage devroom.
- [Join the room interactively (video and chat)](https://chat.fosdem.org/#/room/%23sds-devroom:fosdem.org)
- [Join the room as a spectator (video only)](https://live.fosdem.org/watch/dsds)
- [Event details on the FOSDEM'22 website](https://fosdem.org/2022/schedule/event/sds_garage_introduction)
And if you are not so much of a technical person, but you're dreaming of
a more ethical and emancipatory digital world,
keep in tune with news comming from the Deuxfleurs association
as we will likely have other events very soon!

View file

@ -0,0 +1,170 @@
+++
title="Introducing Garage, our self-hosted distributed object storage solution"
date=2022-02-01
+++
*Deuxfleurs is a non-profit based in France that aims to defend and promote
individual freedom and rights on the Internet. In their quest to build a
decentralized, resilient self-hosting infrastructure, they have found that
currently existing software is often ill suited to such a particular deployment
scenario. In the context of data storage, Garage was built to provide a highly
available data store that exploits redundancy over different geographical
locations, and does its best to not be too impacted by network latencies.*
<!-- more -->
---
Hello! We are Deuxfleurs, a non-profit based in France working to promote
self-hosting and small-scale hosting.
What does that mean? Well, we figured that big tech monopolies such as Google,
Facebook or Amazon today hold disproportionate power and are becoming quite
dangerous to us, citizens of the Internet. They know everything we are doing,
saying, and even thinking, and they are not making good use of that
information. The interests of these companies are those of the capitalist
elite: they are mostly interested in making huge profits by exploiting the
Earth's precious resources, producing, advertising and selling us massive
amounts of stuff we don't need. They don't truly care about the needs of the
people, nor do they care that planetary destruction is under way because of
them.
Big tech monopolies are in a particularly strong position to influence our
behaviors, consciously or not, because we rely on them for selecting the online
content we read, watch, or listen to. Advertising is omnipresent, and because
they know us so well, they can subvert us into thinking that a mindless
consumer society is what we truly want, whereas we most likely would choose
otherwise if we had the chance to think by ourselves.
We don't want that. That's not what the Internet is for. Freedom is freedom
from influence: the ability to do things by oneself, for oneself, on one's own
terms. Self-hosting is both the means by which we reclaim this freedom on the
Internet by not using services of big tech monopolies and thus removing
ourselves from their influence and the result of applying our critical
thinking and our technical abilities to build the Internet that suits us.
Self-hosting means that we don't use cloud services. Instead, we store our
personal data on computers that we own, which we run at home. We build local
communities to share the services that we run with non-technical people. We
communicate with other groups that do the same (or, sometimes, that don't)
thanks to standard protocols such as HTTP, e-mail, or Matrix, that allow a
global community to exist outside of big tech monopolies.
### Self-hosting is a hard problem
As I said, self-hosting means running our own hardware at home, and providing
24/7 Internet services from there. We have many reasons for doing this. One is
because this is the only way we can truly control who has access to our data.
Another one is that it helps us be aware of the physical substrate of which the
Internet is made: making the Internet run has an environmental cost which we
want to evaluate and keep under control. The physical hardware also gives us a
sense of community, calling to mind all of the people that could currently be
connected and making use of our services, and reminding us of the purpose for
which we are doing this.
If you have a home, you know that bad things can happen there too. The power
grid is not infallible, neither is your Internet connection. Fires and floods
happen. And the computers we are running can themselves crash at any moment,
for any number of reasons. Self-hosted solutions today are often not equipped
to face such challenges, and might suffer from unavailability or data loss
as a consequence.
If we want to grow our communities, and attract more people that might be
sympathetic to our vision of the world, we need a baseline of quality for the
services we provide. Users can tolerate some flaws or imperfections, in the
name of defending and promoting their ideals, but if the services are
catastrophic, being unavailable at critical times, or losing users' precious
data, the compromise is much harder to make and people will be tempted to go
back to a comfortable lifestyle bestowed by big tech companies.
Fixing availability, making services reliable even when hosted at unreliable
locations or on unreliable hardware, is one of the main objectives of
Deuxfleurs, and in particular of the project Garage which we are building.
### Distributed systems to the rescue
Distributed systems, or distributed computing, is a set of techniques that can
be applied to make computer services more reliable, by making them run on
several computers at once. It so happens that a few of us have studied
distributed systems, which helps a lot (some of us even have PhDs!)
The following concepts of distributed computing are particularly relevant to
us:
- **Crash tolerance** is when a service that runs on several computers at once
can continue operating normally even when one (or a small number) of the
computers stops working.
- **Geo-distribution** is when the computers that make up a distributed system
are not all located in the same facility. Ideally, they would even be spread
over different cities, so that outages affecting one region do not prevent
the rest of the system from working.
We set out to apply these concepts at Deuxfleurs to build our infrastructure,
in order to provide services that are replicated over several machines in several
geographical locations, so that we are able to provide good availability guarantees
to our users. We try to use as most as possible software packages that already
existed and are freely available, for example the Linux operating system
and the HashiCorp suite (Nomad and Consul).
Unfortunately, in the domain of distributed data storage, the available options
weren't entirely satisfactory in our case, which is why we launched the
development of our own solution: Garage. We will talk more in other blog
posts about why Garage is better suited to us than alternative options. In this
post, I will simply try to give a high-level overview of what Garage is.
### What is Garage, exactly?
Garage is a distributed storage solution, that automatically replicates your
data on several servers. Garage takes into account the geographical location
of servers, and ensures that copies of your data are located at different
locations when possible for maximal redundancy, a unique feature in the
landscape of distributed storage systems.
Garage implements the Amazon S3 protocol, a de-facto standard that makes it
compatible with a large variety of existing software. For instance it can be
used as a storage back-end for many self-hosted web applications such as
NextCloud, Matrix, Mastodon, Peertube, and many others, replacing the local
file system of a server by a distributed storage layer. Garage can also be
used to synchronize your files or store your backups with utilities such as
Rclone or Restic. Last but not least, Garage can be used to host static
websites, such as the one you are currently reading, which is served directly
by the Garage cluster we host at Deuxfleurs.
Garage leverages the theory of distributed systems, and in particular
*Conflict-free Replicated Data Types* (CRDTs in short), a set of mathematical
tools that help us write distributed software that runs faster, by avoiding
some kinds of unnecessary chit-chat between servers. In a future blog post,
we will show how this allow us to significantly outperform Minio, our closest
competitor (another self-hostable implementation of the S3 protocol).
On the side of software engineering, we are committed to making Garage
a tool that is reliable, lightweight, and easy to administrate.
Garage is written in the Rust programming language, which helps us ensure
the stability and safety of the software, and allows us to build software
that is fast and uses little memory.
### Conclusion
The current version of Garage is version 0.6, which is a *beta* release.
This means that it hasn't yet been tested by many people, and we might have
ignored some edge cases in which it would not perform as expected.
However, we are already actively using Garage at Deuxfleurs for many uses, and
it is working exceptionally well for us. We are currently using it to store
backups of personal files, to store the media files that we send and receive
over the Matrix network, as well as to host a small but increasing number of
static websites. Our current deployment hosts about 200 000 files spread in 50
buckets, for a total size of slightly above 500 GB. These number can seem small
when compared to the datasets you could expect your typical cloud provider to
be handling, however these sizes are fairly typical of the small-scale
self-hosted deployments we are targeting, and our Garage cluster is in no way
nearing its capacity limit.
Today, we are proudly releasing Garage's new website, with updated
documentation pages. Poke around to try to understand how the software works,
and try installing your own instance! Your feedback is precious to us, and we
would be glad to hear back from you on our
[issue tracker](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues), by
[e-mail](mailto:garagehq@deuxfleurs.fr), or on our
[Matrix channel](https://matrix.to/#/%23garage:deuxfleurs.fr) (`#garage:deuxfleurs.fr`).

View file

@ -0,0 +1,62 @@
+++
title="Our roadmap towards v1.0"
date=2022-04-10
+++
*a*
<!-- more -->
---
While we hope that Garage in its current state inspired you, we also understand that you may be curious about what will come next!
As per our road towards v1.0, we identified the remaining work and categorized it under one of these 3 domains: *Feature completeness*, *Understandability and manageability*, and *Correctness*.
## Feature completeness
Most importantly, we still need to fix some corner cases on advanced S3 endpoints (eg. [#263](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues), [#248](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/248), [#204](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/204)). Based on community feedbacks, we might also consider implementing additional endpoints (eg. [#166](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/166)) or quotas (eg. [#71](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/71)) but we can't make any promise (sorry!).
We also made a series of observation (the S3 API is not adapted to handle multiple small objects, many software require a database, we already have an internal database for metadata) which makes us believe that Garage could leverage its internal database system to provide a simple key-value store. We have already written [an API draft](https://p.adnab.me/code/#/2/code/view/eUNPbfoUrMbCY+CoMXaqed4jmWlmvWALHNDcfuM-O5o/embed/present/) for an API we named K2V. In the following months, we will then try to implement it and merge it if it makes sense. Spiritually, we would like it to be close to the original [Amazon Dynamo paper](https://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf), or if you prefer approximative comparisons, K2V could be to Cassandra what sqlite is to PostgreSQL.
If you worry about feature bloat, be ensured that we do not plan to extend Garage beyond these points!
## Understandability and manageability
Based on community feedback (eg. [#228](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/228), [#221](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/221), [#217](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/217)), we have identified some points on which we want to dedicate work: *Consistency Model*, *Administration*, *Storage Model*, and *Ecosystem*.
Garage has currently 2 API: S3 and Admin, with different *Consistency Models*. S3 API's consistency is aligned on [Amazon's new S3 Strong Consistency](https://aws.amazon.com/s3/consistency/) while Admin API's eventual consistency is not yet specified. We want to document first the Admin API's consistency, then we could make S3 consistency explanation more approachable.
The Admin API is also currently only exposed through our custom RPC endpoint. We would like to expose it through a REST endpoint ([#231](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/231)) to ease *Administration*. This REST API would make possible to build a web interface to manage Garage ([#232](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/232)).
When people discover Garage, questions also arise around its *Storage Model*, eg. what reliability and density shoud they expect depending on their considered configuration. Ideally, we would like to write a calculator similar to [Minio's one](https://min.io/product/erasure-code-calculator) to help Garage operators understand the properties of their cluster.
Finally, Garage exists in an *Ecosystem*, consisting of multiple alternative software (eg. Ceph, Minio, SeaweedFS, IPFS Cluster), with different needs (research, customer services, archiving), made of various hardware (from a Raspberry Pi to a 64+ disk blade server). We want to situate Garage in its ecosystem while identifying, promoting and documenting some "Garage success story".
<!--2) Explaining how Garage can take its place in the existing ecosystem, including among the other distributed storage systems, but also in term of uses cases and deployments (how does it perform at scale, with which hardware, for which application, etc.) 3) Make possible to manage Garage from a REST API, possibly write a web GUI to make administration easier, 4) help people understand the reliability and storage density they will have for a specific Garage deployment, if possible through a simulator, 5) we might consider adding a system of quota to protect a cluster from a misbehaving user. 6) Integration in ecosystems -->
## Correctness
We know in theory that Garage's design works and horizontally scales.
But we still need to make sure that in practise our implementation is correct, and thus features these defined properties.
[Jepsen](https://jepsen.io/) is a well-known tool to test distributed system properties by simulating some system states and sequencing packets in a specific order.
We would like to learn it and apply it to Garage to better convince ourselves that we
Consider control theory, speak about tranquilizer ([#145](https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/145)).
Horizontal scaling made possible no leader also no erasure coding.
Track bottlenecks, instability and overload.
We also plan to deploy Garage on multiple clusters and do a large serie of benchmarks.
<!-- non goals: erasure coding, byzantine tolerance ; do we want quota? -->
## Conclusion
Please note that this roadmap is purely indicative, we are not committing to deliver these features.
Contributions are however welcome, so get in touch (on our Matrix channel) if you want to implement one of them.
We also don't know when v1.0 will be released, except "when it will be ready", but we would be happy if it could be by the end of 2022.
See you soon for the next release!

View file

@ -1,57 +0,0 @@
+++
title="Aerogramme was at FOSDEM'24 modern email track"
date=2024-02-05
+++
*FOSDEM is the European conference for FLOSS developpers. It was the first time Aerogramme was discussed publicly.
If you missed the presentation, a recording and the slides are available.*
<!-- more -->
---
## Recording
<div class="video">
<video controls="controls" width="75%">
<source src="https://video.fosdem.org/2024/h2213/fosdem-2024-2642--servers-aerogramme-a-multi-region-imap-server.av1.webm" type="video/webm; codecs=&quot;av01.0.08M.08.0.110.01.01.01.0&quot;">
<source src="https://video.fosdem.org/2024/h2213/fosdem-2024-2642--servers-aerogramme-a-multi-region-imap-server.mp4" type="video/mp4">
</video>
</div>
## Transcript
*(slide 1)* Aerogramme is a multi-region IMAP server, in this talk we will discuss what a "multi-region IMAP server" means and why it's important.
*(slide 2)*
Let's start with some context, my name is Quentin. I have a PhD in distributed system,
and this talk will be a lot about PhD distributed system because something I know.
I try to work as much as possible for a collective named Deuxfleurs where we try to build a low-tech Internet.
If you want to know more about our project, check yesterday talk about Garage.
Aerogramme is part of Deuxfleurs' strategy, and a very nice thing, this project is supported by NLnet.
*(slide 3)*
First, the problem we want to solve: we want to make other people available when it would be otherwise
impossible (due to geographical distances for example). We can achieve this goal only if the underlying system is working:
so we will talk a lot about availability and reliability.
*(slide 4)*
Today's talk is about 3 main ideas: 1) we should not trust the cloud & hosting providers as they can fail. 2) there is some space to explore alternative IMAP server designs and 3) and finally I will try to convince you that such new designs can work in the real life
*(slide 5)*
In the title talk, I speak about "multi-region", and so we must define first what is a "region".
On the slide is depicted the Google Cloud Platform (GCP) region in Paris: it's made of 3 datacenters.
Last april, the whole region, the 3 datacenters were unavailable for 3 weeks.
The outage lasted for 3 weeks for some services, and it was due to a fire in one datacenter.
And due to tight connections between the 3 datacenters, the 2 others ones were unusable due to a software problem.
3 weeks without emails, you imagine it can make your life very hard if you need to do some important stuff, like seeking a new job.
*(rest of the transcript is missing currently)*
## Links
- [Download Slides](https://fosdem.org/2024/events/attachments/fosdem-2024-2642--servers-aerogramme-a-multi-region-imap-server/slides/22665/aerogramme_vmnrj3Q.pdf)
- [Aerogramme Talk Page](https://fosdem.org/2024/schedule/event/fosdem-2024-2642--servers-aerogramme-a-multi-region-imap-server/)
- [Modern Email Track](https://fosdem.org/2024/schedule/track/modern-email/)
- [2024 edition main page](https://fosdem.org/2024/)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

View file

@ -1,117 +0,0 @@
+++
title="Aerogramme 0.2.2: predictability & user testing"
date=2024-02-22
+++
*Let's review how Aerogramme performances became more predictable, why it's important, and showcase how user testing helped surfacing bugs.*
<!-- more -->
---
This minor version of Aerogramme put the focus on 2 aspects of the software: predictable performances & collecting user feedbacks.
In the following, I describe both aspect in details.
## Improving predictability
In the previous blog post, we asked ourselves [Does Aerogramme use too much memory?](@/blog/2024-ram-usage-encryption-s3/index.md).
From the discussion, we surfaced it was not acceptable that some specific queries (FETCH FULL or SEARCH TEXT) where loading the
full mailbox in memory. It's concerning as the mail server will be used by multiple users and as a limited amount of resources,
so we don't want a user allocating all the memory. In other words, we want to have a per-user resource usage that remain as stable as possible.
These ideas are developed more in depth in the Amazon article [Reliability & Constant Work](https://aws.amazon.com/fr/builders-library/reliability-and-constant-work/).
As part of the conclusion, we identified that streaming emails content would solve our problem.
In practice, I rewrote the relevant part of the code to return a `futures::stream::Stream` instead of a `Vec`.
Then, for both requests, I re-run the same benchmark to have a before/after comparison for both commands.
Next, the first pictures is for FETCH FULL, the second one for SEARCH TEXT.
![Fetch resource usage for Aerograme 0.2.1 & 0.2.2](fetch-full.png)
![Search resource usage for Aerograme 0.2.1 & 0.2.2](search-full.png)
For both FETCH and SEARCH, the changes are identical. Before, the command was executed in ~5 seconds, allocated up to 300MB of RAM, and used up to 150% of CPU.
After, the command took ~30 to get executed, allocated up to 400MB of RAM, used up to 40MB of RAM, and used up to 80% of CPU sporadically.
Here is why it's a positive thing: now the memory consumption of Aerogramme is capped approximately by the biggest email accepted
by your system (25MB, multiplied by a small constant).
It has also other benefits: it prevents the file descriptor and other network ressource exhaustion,
and it adds fairness between users. Indeed, a user can't monopolize all the ressources of the servers (CPU, I/O, etc.) anymore, and thus multiple users
requests are thus intertwined by the server (we assume the number of users is greatly superior to the number of cores). And again, it leads to better predictability, as per-user requests completion will be less impacted by other requests.
Another concern is the RAM consumption with the IDLE feature.
The real cause is that we retain in-memory the full user profile (mailbox data, IO connectors, etc.): we should instead
keep only the minimum data to be waken-up. That's the ideal fix, the final solution, that would take lots of time to design and implement.
This fix is not necessary *now*, instead we can simply try to optimize the size of a full user profile in memory.
On this aspect, the [aws-sdk-s3](https://crates.io/crates/aws-sdk-s3) crate has [the following note](https://docs.rs/aws-sdk-s3/1.16.0/aws_sdk_s3/client/index.html):
> Client construction is expensive due to connection thread pool initialization, and should be done once at application start-up.
Digging deeper in the crate dependencies, we learn from the [aws-smithy-runtime](https://crates.io/crates/aws-smithy-runtime) crate, [we can read](https://docs.rs/aws-smithy-runtime/1.1.7/aws_smithy_runtime/client/http/hyper_014/struct.HyperClientBuilder.html):
> [Constructing] a Hyper client with the default TLS implementation (rustls) [...] can be useful when you want to share a Hyper connector between multiple generated Smithy clients.
It seems to be exactly what we want to do, but to be effective, we must do the same thing for K2V.
After implementing these features, I got the following plot:
![Idle resource usage for Aerograme 0.2.1 & 0.2.2](idle.png)
First, the spikes are more spaced because I am typing the command by hands, not because Aerogramme is slower!
Another non relevant artifact is the end of the memory plot: memory is not released on the left plot as I cut the recording before typing the LOGOUT command.
What's interesting is the memory usage range: on the left, it's ~20MB, on the right it's ~10MB.
By sharing the HTTP Client, we thus use twice less memory per-user, down to around ~650kB/user for Aerogramme 0.2.2
Back to our 1k users, we get 650MB of RAM, 6.5GB for 10k, and thus 65GB for 100k. So *in theory*, it seems OK,
but our sample and our methodology is too dubious to confirm that *in practice* such memory usage will be observed.
In the end, I think for now IDLE RAM usage in Aerogramme is acceptable, and thus we can move
on other aspects without fearing that IDLE will make the software unusable.
## Users feedbacks
*Dovecot AUTH continuation inlining* - When a username + password is short,
the Dovecot SASL Auth protocol allows the client (here Postfix)
to send the base64 inlined, without having to wait for the continuation.
It was not supported by Aerogramme and was preventing some users from authenticating.
*Pipelining limits (reported by Nicolas)* - Pipeling limit set to 3.
Avoiding DoS resources. But failing some honest clients like Mutt.
Bumped to 64, will be watched in the next months.
*SASL Auth subtleties (reported by Nicolas)* - Authorization can be empty, or can be set to the same value as Authentication.
Second case not handled but required by Fair Email (thx Nicolas)
*Thunderbird Autodiscovery issues (reported by LX & Nicolas)* - K9 stable does not support the `%EMAILLOCALPART%` placeholder.
K9 beta (6.714) does not support some values marked as obsolete in the `authentication` field: `plain` is not supported anymore, `password-cleartext` must be used instead.
Content-Type is important also, if a wrong one is sent, content is silently ignored by some clients.
Today autodiscovery is not part of Aerogramme, but due to these issues, and given that generating these files
on the fly could improve compatibility (email is passed a query parameter), so we could directly put the right username
instead of relying on placeholders that are not well supported.
*Broken LITERAL+ (reported by Maxime)* - It was not possible to copy more than one email at once to an Aerogramme mailbox.
It was due to the fact we were using an old version of imap-flow that was not correctly supporting LITERAL+.
Upgrading imap-flow to the latest version fixed the problem.
*Broken IDLE (reported by Maxime)* - After updating imap-flow, we started noticing some timeouts in Thunderbird due to IDLE bugs.
When IDLE was implemented in Aerogramme, the code was not ready in imap-flow,
and thus I used some hacks. But upgrading the library broke my hacks for the best: now
imap-flow supports IDLE out of the box, and thus Aerogramme code is now cleaner and more maintainable.
Some others quality of life feedbacks not reported here have been made by MrFlos & Nicolas, thanks to all the people that took part in this debugging adventure.
## Conclusion: download and test this new version!
Do not get me wrong: Aerogramme is still not ready for prime time.
But by operating it for real, we start understanding better how it behaves, what are the rough edges, what we want to improve, what we need, etc.
If you are interested in being an Aerogramme early adopter, it might be the good time to setup a test cluster, try Aerogramme,
share your use cases, in order to shape its future and be ready to go in production when its public beta will be announced.
```bash
docker run --net host registry.deuxfleurs.org/aerogramme:0.2.2
```
*It will launch Aerogramme in development mode. Username is `alice`, password is `hunter2`, email is `alice@example.tld`.*
[Download](/download/) - [Changelog](https://git.deuxfleurs.fr/Deuxfleurs/aerogramme/releases/tag/0.2.2) - [Getting started](/documentation/)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

View file

@ -1,693 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1049" height="450" viewBox="0 0 1049 450">
<defs>
<g>
<g id="glyph-0-0">
<path d="M 5.671875 -1.953125 L 5.671875 -6.40625 L 4.859375 -6.40625 L 4.859375 -1.953125 C 4.859375 -1.09375 4.234375 -0.5625 3.203125 -0.5625 C 2.25 -0.5625 1.5625 -1.015625 1.5625 -1.953125 L 1.5625 -6.40625 L 0.75 -6.40625 L 0.75 -1.953125 C 0.75 -0.65625 1.6875 0.15625 3.203125 0.15625 C 4.703125 0.15625 5.671875 -0.671875 5.671875 -1.953125 Z M 5.671875 -1.953125 "/>
</g>
<g id="glyph-0-1">
<path d="M 4.28125 0 L 4.28125 -3.484375 C 4.28125 -4.25 3.71875 -4.734375 2.828125 -4.734375 C 2.140625 -4.734375 1.703125 -4.484375 1.296875 -3.828125 L 1.296875 -4.609375 L 0.609375 -4.609375 L 0.609375 0 L 1.34375 0 L 1.34375 -2.546875 C 1.34375 -3.484375 1.859375 -4.09375 2.609375 -4.09375 C 3.1875 -4.09375 3.546875 -3.75 3.546875 -3.1875 L 3.546875 0 Z M 4.28125 0 "/>
</g>
<g id="glyph-0-2">
<path d="M 1.34375 0 L 1.34375 -4.609375 L 0.609375 -4.609375 L 0.609375 0 Z M 1.4375 -5.296875 L 1.4375 -6.21875 L 0.53125 -6.21875 L 0.53125 -5.296875 Z M 1.4375 -5.296875 "/>
</g>
<g id="glyph-0-3">
<path d="M 4.359375 1.921875 L 4.359375 -4.609375 L 3.703125 -4.609375 L 3.703125 -4 C 3.359375 -4.484375 2.84375 -4.734375 2.234375 -4.734375 C 1.015625 -4.734375 0.234375 -3.78125 0.234375 -2.25 C 0.234375 -0.75 0.984375 0.125 2.203125 0.125 C 2.84375 0.125 3.28125 -0.09375 3.625 -0.59375 L 3.625 1.921875 Z M 3.625 -2.28125 C 3.625 -1.21875 3.109375 -0.546875 2.34375 -0.546875 C 1.53125 -0.546875 1 -1.234375 1 -2.3125 C 1 -3.375 1.53125 -4.0625 2.34375 -4.0625 C 3.125 -4.0625 3.625 -3.359375 3.625 -2.28125 Z M 3.625 -2.28125 "/>
</g>
<g id="glyph-0-4">
<path d="M 4.234375 0 L 4.234375 -4.609375 L 3.515625 -4.609375 L 3.515625 -2.0625 C 3.515625 -1.125 3.015625 -0.515625 2.25 -0.515625 C 1.671875 -0.515625 1.296875 -0.859375 1.296875 -1.421875 L 1.296875 -4.609375 L 0.578125 -4.609375 L 0.578125 -1.125 C 0.578125 -0.359375 1.140625 0.125 2.046875 0.125 C 2.71875 0.125 3.15625 -0.109375 3.578125 -0.71875 L 3.578125 0 Z M 4.234375 0 "/>
</g>
<g id="glyph-0-5">
<path d="M 4.515625 -2.09375 C 4.515625 -2.765625 4.453125 -3.1875 4.328125 -3.53125 C 4.03125 -4.28125 3.328125 -4.734375 2.46875 -4.734375 C 1.171875 -4.734375 0.359375 -3.796875 0.359375 -2.28125 C 0.359375 -0.765625 1.15625 0.125 2.453125 0.125 C 3.5 0.125 4.234375 -0.46875 4.421875 -1.40625 L 3.671875 -1.40625 C 3.46875 -0.796875 3.0625 -0.546875 2.46875 -0.546875 C 1.703125 -0.546875 1.140625 -1.03125 1.125 -2.09375 Z M 3.734375 -2.75 C 3.734375 -2.75 3.734375 -2.703125 3.71875 -2.6875 L 1.140625 -2.6875 C 1.203125 -3.515625 1.71875 -4.0625 2.453125 -4.0625 C 3.171875 -4.0625 3.734375 -3.46875 3.734375 -2.75 Z M 3.734375 -2.75 "/>
</g>
<g id="glyph-0-6">
<path d="M 5.96875 0 L 5.96875 -0.203125 C 5.671875 -0.40625 5.59375 -0.640625 5.59375 -1.5 C 5.5625 -2.546875 5.40625 -2.875 4.71875 -3.171875 C 5.4375 -3.515625 5.734375 -3.96875 5.734375 -4.703125 C 5.734375 -5.8125 5.03125 -6.40625 3.78125 -6.40625 L 0.8125 -6.40625 L 0.8125 0 L 1.640625 0 L 1.640625 -2.765625 L 3.75 -2.765625 C 4.46875 -2.765625 4.8125 -2.40625 4.796875 -1.625 L 4.796875 -1.046875 C 4.78125 -0.65625 4.859375 -0.265625 4.984375 0 Z M 4.875 -4.578125 C 4.875 -3.828125 4.484375 -3.484375 3.609375 -3.484375 L 1.640625 -3.484375 L 1.640625 -5.6875 L 3.609375 -5.6875 C 4.53125 -5.6875 4.875 -5.296875 4.875 -4.578125 Z M 4.875 -4.578125 "/>
</g>
<g id="glyph-0-7">
<path d="M 4.703125 -0.015625 L 4.703125 -0.578125 C 4.625 -0.546875 4.59375 -0.546875 4.546875 -0.546875 C 4.296875 -0.546875 4.15625 -0.6875 4.15625 -0.921875 L 4.15625 -3.484375 C 4.15625 -4.296875 3.546875 -4.734375 2.421875 -4.734375 C 1.296875 -4.734375 0.609375 -4.3125 0.578125 -3.25 L 1.3125 -3.25 C 1.375 -3.8125 1.703125 -4.0625 2.390625 -4.0625 C 3.046875 -4.0625 3.421875 -3.8125 3.421875 -3.375 L 3.421875 -3.1875 C 3.421875 -2.875 3.234375 -2.75 2.65625 -2.671875 C 1.625 -2.546875 1.453125 -2.5 1.171875 -2.390625 C 0.640625 -2.171875 0.375 -1.765625 0.375 -1.203125 C 0.375 -0.359375 0.953125 0.125 1.875 0.125 C 2.46875 0.125 3.046875 -0.109375 3.453125 -0.546875 C 3.53125 -0.1875 3.84375 0.0625 4.203125 0.0625 C 4.359375 0.0625 4.46875 0.046875 4.703125 -0.015625 Z M 3.421875 -1.59375 C 3.421875 -0.9375 2.75 -0.515625 2.046875 -0.515625 C 1.46875 -0.515625 1.140625 -0.71875 1.140625 -1.21875 C 1.140625 -1.703125 1.453125 -1.90625 2.25 -2.015625 C 3.015625 -2.125 3.171875 -2.15625 3.421875 -2.28125 Z M 3.421875 -1.59375 "/>
</g>
<g id="glyph-0-8">
<path d="M 6.234375 -4.609375 L 5.40625 -4.609375 L 4.484375 -1.015625 L 3.578125 -4.609375 L 2.6875 -4.609375 L 1.796875 -1.015625 L 0.859375 -4.609375 L 0.046875 -4.609375 L 1.390625 0 L 2.21875 0 L 3.109375 -3.609375 L 4.03125 0 L 4.875 0 Z M 6.234375 -4.609375 "/>
</g>
<g id="glyph-0-9">
<path d="M 5.75 0 L 3.5 -6.40625 L 2.4375 -6.40625 L 0.15625 0 L 1.015625 0 L 1.703125 -1.921875 L 4.171875 -1.921875 L 4.828125 0 Z M 3.9375 -2.609375 L 1.90625 -2.609375 L 2.953125 -5.53125 Z M 3.9375 -2.609375 "/>
</g>
<g id="glyph-0-10">
<path d="M 4.59375 -2.265625 C 4.59375 -3.8125 3.84375 -4.734375 2.625 -4.734375 C 2 -4.734375 1.5 -4.453125 1.15625 -3.921875 L 1.15625 -4.609375 L 0.484375 -4.609375 L 0.484375 1.921875 L 1.21875 1.921875 L 1.21875 -0.546875 C 1.59375 -0.078125 2.03125 0.125 2.625 0.125 C 3.8125 0.125 4.59375 -0.796875 4.59375 -2.265625 Z M 3.828125 -2.28125 C 3.828125 -1.234375 3.296875 -0.546875 2.5 -0.546875 C 1.71875 -0.546875 1.21875 -1.1875 1.21875 -2.265625 C 1.21875 -3.359375 1.71875 -4.0625 2.5 -4.0625 C 3.3125 -4.0625 3.828125 -3.375 3.828125 -2.28125 Z M 3.828125 -2.28125 "/>
</g>
<g id="glyph-0-11">
<path d="M 4.359375 0 L 4.359375 -6.40625 L 3.625 -6.40625 L 3.625 -4.03125 C 3.3125 -4.5 2.828125 -4.734375 2.203125 -4.734375 C 1.015625 -4.734375 0.234375 -3.8125 0.234375 -2.34375 C 0.234375 -0.796875 1 0.125 2.234375 0.125 C 2.875 0.125 3.3125 -0.109375 3.703125 -0.671875 L 3.703125 0 Z M 3.625 -2.28125 C 3.625 -1.21875 3.109375 -0.546875 2.34375 -0.546875 C 1.53125 -0.546875 1 -1.234375 1 -2.3125 C 1 -3.375 1.53125 -4.0625 2.328125 -4.0625 C 3.125 -4.0625 3.625 -3.359375 3.625 -2.28125 Z M 3.625 -2.28125 "/>
</g>
<g id="glyph-0-12">
<path d="M 5.953125 -2.34375 L 5.109375 -2.34375 C 4.921875 -1.125 4.40625 -0.5625 3.328125 -0.5625 C 2.046875 -0.5625 1.234375 -1.546875 1.234375 -3.140625 C 1.234375 -4.78125 2.015625 -5.84375 3.25 -5.84375 C 4.25 -5.84375 4.78125 -5.40625 4.984375 -4.421875 L 5.828125 -4.421875 C 5.5625 -5.828125 4.765625 -6.578125 3.359375 -6.578125 C 1.40625 -6.578125 0.421875 -5 0.421875 -3.125 C 0.421875 -1.265625 1.4375 0.15625 3.3125 0.15625 C 4.875 0.15625 5.765625 -0.671875 5.953125 -2.34375 Z M 5.953125 -2.34375 "/>
</g>
<g id="glyph-0-13">
<path d="M 4.59375 -2.359375 C 4.59375 -3.859375 3.84375 -4.734375 2.625 -4.734375 C 2 -4.734375 1.546875 -4.5 1.203125 -3.984375 L 1.203125 -6.40625 L 0.46875 -6.40625 L 0.46875 0 L 1.140625 0 L 1.140625 -0.65625 C 1.484375 -0.125 1.953125 0.125 2.59375 0.125 C 3.8125 0.125 4.59375 -0.859375 4.59375 -2.359375 Z M 3.828125 -2.3125 C 3.828125 -1.265625 3.296875 -0.546875 2.484375 -0.546875 C 1.71875 -0.546875 1.203125 -1.265625 1.203125 -2.3125 C 1.203125 -3.390625 1.71875 -4.09375 2.484375 -4.0625 C 3.3125 -4.0625 3.828125 -3.34375 3.828125 -2.3125 Z M 3.828125 -2.3125 "/>
</g>
<g id="glyph-0-14">
<path d="M 1.328125 0 L 1.328125 -6.40625 L 0.59375 -6.40625 L 0.59375 0 Z M 1.328125 0 "/>
</g>
<g id="glyph-0-15">
<path d="M 2.234375 0 L 2.234375 -0.609375 C 2.140625 -0.59375 2.015625 -0.578125 1.875 -0.578125 C 1.5625 -0.578125 1.484375 -0.671875 1.484375 -1 L 1.484375 -4.015625 L 2.234375 -4.015625 L 2.234375 -4.609375 L 1.484375 -4.609375 L 1.484375 -5.875 L 0.75 -5.875 L 0.75 -4.609375 L 0.125 -4.609375 L 0.125 -4.015625 L 0.75 -4.015625 L 0.75 -0.671875 C 0.75 -0.203125 1.0625 0.0625 1.640625 0.0625 C 1.8125 0.0625 1.984375 0.046875 2.234375 0 Z M 2.234375 0 "/>
</g>
<g id="glyph-0-16">
<path d="M 4.203125 -4.609375 L 3.40625 -4.609375 L 2.140625 -1.015625 L 0.953125 -4.609375 L 0.171875 -4.609375 L 1.734375 0.015625 L 1.453125 0.75 C 1.328125 1.078125 1.171875 1.203125 0.859375 1.203125 C 0.75 1.203125 0.640625 1.171875 0.46875 1.140625 L 0.46875 1.796875 C 0.625 1.875 0.78125 1.921875 0.96875 1.921875 C 1.484375 1.921875 1.90625 1.625 2.15625 0.96875 Z M 4.203125 -4.609375 "/>
</g>
<g id="glyph-0-17">
<path d="M 4.28125 0 L 4.28125 -3.484375 C 4.28125 -4.25 3.71875 -4.734375 2.828125 -4.734375 C 2.171875 -4.734375 1.78125 -4.546875 1.34375 -3.96875 L 1.34375 -6.40625 L 0.609375 -6.40625 L 0.609375 0 L 1.34375 0 L 1.34375 -2.546875 C 1.34375 -3.484375 1.84375 -4.09375 2.59375 -4.09375 C 3.109375 -4.09375 3.546875 -3.796875 3.546875 -3.1875 L 3.546875 0 Z M 4.28125 0 "/>
</g>
<g id="glyph-0-18">
<path d="M 4.203125 -1.578125 L 3.453125 -1.578125 C 3.328125 -0.84375 2.953125 -0.546875 2.328125 -0.546875 C 1.515625 -0.546875 1.03125 -1.171875 1.03125 -2.265625 C 1.03125 -3.40625 1.515625 -4.0625 2.3125 -4.0625 C 2.921875 -4.0625 3.3125 -3.703125 3.40625 -3.0625 L 4.140625 -3.0625 C 4.0625 -4.1875 3.359375 -4.734375 2.328125 -4.734375 C 1.078125 -4.734375 0.265625 -3.796875 0.265625 -2.265625 C 0.265625 -0.78125 1.0625 0.125 2.3125 0.125 C 3.40625 0.125 4.109375 -0.53125 4.203125 -1.578125 Z M 4.203125 -1.578125 "/>
</g>
<g id="glyph-0-19">
<path d="M 4.421875 0 L 2.53125 -3.015625 L 4.140625 -4.609375 L 3.1875 -4.609375 L 1.234375 -2.65625 L 1.234375 -6.40625 L 0.515625 -6.40625 L 0.515625 0 L 1.234375 0 L 1.234375 -1.796875 L 1.953125 -2.5 L 3.515625 0 Z M 4.421875 0 "/>
</g>
<g id="glyph-0-20">
<path d="M 4.484375 -2.265625 C 4.484375 -3.859375 3.71875 -4.734375 2.390625 -4.734375 C 1.09375 -4.734375 0.3125 -3.859375 0.3125 -2.3125 C 0.3125 -0.75 1.09375 0.125 2.40625 0.125 C 3.6875 0.125 4.484375 -0.75 4.484375 -2.265625 Z M 3.71875 -2.28125 C 3.71875 -1.203125 3.203125 -0.546875 2.40625 -0.546875 C 1.578125 -0.546875 1.078125 -1.1875 1.078125 -2.3125 C 1.078125 -3.40625 1.578125 -4.0625 2.40625 -4.0625 C 3.234375 -4.0625 3.71875 -3.421875 3.71875 -2.28125 Z M 3.71875 -2.28125 "/>
</g>
<g id="glyph-0-21">
<path d="M 4.03125 -1.296875 C 4.03125 -1.984375 3.65625 -2.328125 2.734375 -2.546875 L 2.03125 -2.703125 C 1.4375 -2.84375 1.171875 -3.046875 1.171875 -3.375 C 1.171875 -3.796875 1.5625 -4.0625 2.15625 -4.0625 C 2.75 -4.0625 3.0625 -3.8125 3.078125 -3.328125 L 3.859375 -3.328125 C 3.84375 -4.234375 3.25 -4.734375 2.1875 -4.734375 C 1.109375 -4.734375 0.40625 -4.1875 0.40625 -3.328125 C 0.40625 -2.609375 0.78125 -2.265625 1.875 -2 L 2.5625 -1.84375 C 3.0625 -1.71875 3.265625 -1.5625 3.265625 -1.234375 C 3.265625 -0.796875 2.84375 -0.546875 2.203125 -0.546875 C 1.546875 -0.546875 1.171875 -0.703125 1.078125 -1.40625 L 0.296875 -1.40625 C 0.328125 -0.34375 0.9375 0.125 2.140625 0.125 C 3.296875 0.125 4.03125 -0.40625 4.03125 -1.296875 Z M 4.03125 -1.296875 "/>
</g>
<g id="glyph-0-22">
<path d="M 2.828125 -3.96875 L 2.828125 -4.71875 C 2.703125 -4.734375 2.640625 -4.734375 2.546875 -4.734375 C 2.0625 -4.734375 1.703125 -4.453125 1.28125 -3.78125 L 1.28125 -4.609375 L 0.609375 -4.609375 L 0.609375 0 L 1.34375 0 L 1.34375 -2.390625 C 1.34375 -3.4375 1.6875 -3.953125 2.828125 -3.96875 Z M 2.828125 -3.96875 "/>
</g>
<g id="glyph-0-23">
<path d="M 5.390625 0 L 5.390625 -0.71875 L 1.609375 -0.71875 L 1.609375 -2.921875 L 5.109375 -2.921875 L 5.109375 -3.640625 L 1.609375 -3.640625 L 1.609375 -5.6875 L 5.234375 -5.6875 L 5.234375 -6.40625 L 0.796875 -6.40625 L 0.796875 0 Z M 5.390625 0 "/>
</g>
<g id="glyph-0-24">
<path d="M 4.15625 0 L 2.5625 -2.390625 L 4.109375 -4.609375 L 3.296875 -4.609375 L 2.1875 -2.9375 L 1.078125 -4.609375 L 0.234375 -4.609375 L 1.78125 -2.34375 L 0.15625 0 L 0.984375 0 L 2.15625 -1.765625 L 3.3125 0 Z M 4.15625 0 "/>
</g>
<g id="glyph-0-25">
<path d="M 6.6875 0 L 6.6875 -3.453125 C 6.6875 -4.28125 6.234375 -4.734375 5.359375 -4.734375 C 4.75 -4.734375 4.375 -4.5625 3.953125 -4.03125 C 3.671875 -4.53125 3.3125 -4.734375 2.703125 -4.734375 C 2.09375 -4.734375 1.6875 -4.515625 1.296875 -3.953125 L 1.296875 -4.609375 L 0.625 -4.609375 L 0.625 0 L 1.359375 0 L 1.359375 -2.890625 C 1.359375 -3.5625 1.84375 -4.09375 2.4375 -4.09375 C 2.984375 -4.09375 3.296875 -3.765625 3.296875 -3.171875 L 3.296875 0 L 4.015625 0 L 4.015625 -2.890625 C 4.015625 -3.5625 4.515625 -4.09375 5.109375 -4.09375 C 5.640625 -4.09375 5.953125 -3.75 5.953125 -3.171875 L 5.953125 0 Z M 6.6875 0 "/>
</g>
<g id="glyph-0-26">
<path d="M 4.296875 -0.75 L 4.296875 -4.609375 L 3.625 -4.609375 L 3.625 -3.9375 C 3.25 -4.484375 2.8125 -4.734375 2.21875 -4.734375 C 1.046875 -4.734375 0.25 -3.75 0.25 -2.265625 C 0.25 -0.8125 1.078125 0.125 2.15625 0.125 C 2.734375 0.125 3.140625 -0.109375 3.625 -0.6875 L 3.625 -0.390625 C 3.625 0.828125 3.125 1.296875 2.265625 1.296875 C 1.6875 1.296875 1.234375 1.09375 1.15625 0.53125 L 0.40625 0.53125 C 0.484375 1.40625 1.15625 1.921875 2.25 1.921875 C 3.6875 1.921875 4.296875 1.28125 4.296875 -0.75 Z M 3.546875 -2.28125 C 3.546875 -1.171875 3.078125 -0.546875 2.3125 -0.546875 C 1.5 -0.546875 1.015625 -1.1875 1.015625 -2.3125 C 1.015625 -3.40625 1.515625 -4.0625 2.296875 -4.0625 C 3.09375 -4.0625 3.546875 -3.390625 3.546875 -2.28125 Z M 3.546875 -2.28125 "/>
</g>
<g id="glyph-0-27">
<path d="M 5.09375 -5.6875 L 5.09375 -6.40625 L 0.796875 -6.40625 L 0.796875 0 L 1.609375 0 L 1.609375 -2.921875 L 4.671875 -2.921875 L 4.671875 -3.640625 L 1.609375 -3.640625 L 1.609375 -5.6875 Z M 5.09375 -5.6875 "/>
</g>
<g id="glyph-0-28">
<path d="M 1.703125 0 L 1.703125 -6.40625 L 0.875 -6.40625 L 0.875 0 Z M 1.703125 0 "/>
</g>
<g id="glyph-0-29">
<path d="M 4.6875 0 L 4.6875 -0.71875 L 1.515625 -0.71875 L 1.515625 -6.40625 L 0.703125 -6.40625 L 0.703125 0 Z M 4.6875 0 "/>
</g>
<g id="glyph-0-30">
<path d="M 6.6875 0 L 6.6875 -6.40625 L 5.5625 -6.40625 L 3.6875 -0.828125 L 1.796875 -6.40625 L 0.65625 -6.40625 L 0.65625 0 L 1.4375 0 L 1.4375 -5.375 L 3.25 0 L 4.109375 0 L 5.921875 -5.375 L 5.921875 0 Z M 6.6875 0 "/>
</g>
<g id="glyph-0-31">
<path d="M 4.28125 -4.609375 L 3.453125 -4.609375 L 2.140625 -0.875 L 0.921875 -4.609375 L 0.09375 -4.609375 L 1.703125 0 L 2.5 0 Z M 4.28125 -4.609375 "/>
</g>
<g id="glyph-0-32">
<path d="M 5.6875 0 L 5.6875 -6.40625 L 4.90625 -6.40625 L 4.90625 -1.171875 L 1.5625 -6.40625 L 0.671875 -6.40625 L 0.671875 0 L 1.4375 0 L 1.4375 -5.203125 L 4.765625 0 Z M 5.6875 0 "/>
</g>
<g id="glyph-0-33">
<path d="M 5.46875 -1.765625 C 5.46875 -2.546875 4.96875 -3.125 4.09375 -3.375 L 2.484375 -3.796875 C 1.71875 -4 1.4375 -4.25 1.4375 -4.75 C 1.4375 -5.40625 2 -5.890625 2.875 -5.890625 C 3.890625 -5.890625 4.46875 -5.421875 4.46875 -4.578125 L 5.25 -4.578125 C 5.25 -5.84375 4.375 -6.578125 2.890625 -6.578125 C 1.484375 -6.578125 0.609375 -5.796875 0.609375 -4.640625 C 0.609375 -3.859375 1.03125 -3.359375 1.875 -3.140625 L 3.46875 -2.71875 C 4.28125 -2.5 4.640625 -2.1875 4.640625 -1.6875 C 4.640625 -0.96875 4.109375 -0.5625 3.015625 -0.5625 C 1.796875 -0.5625 1.203125 -1.171875 1.203125 -2.078125 L 0.421875 -2.078125 C 0.421875 -0.578125 1.4375 0.15625 2.953125 0.15625 C 4.578125 0.15625 5.46875 -0.609375 5.46875 -1.765625 Z M 5.46875 -1.765625 "/>
</g>
<g id="glyph-0-34">
<path d="M 4.453125 -3 C 4.453125 -5.15625 3.78125 -6.234375 2.421875 -6.234375 C 1.078125 -6.234375 0.375 -5.140625 0.375 -3.046875 C 0.375 -0.953125 1.078125 0.125 2.421875 0.125 C 3.734375 0.125 4.453125 -0.953125 4.453125 -3 Z M 3.671875 -3.0625 C 3.671875 -1.296875 3.265625 -0.515625 2.40625 -0.515625 C 1.578125 -0.515625 1.171875 -1.34375 1.171875 -3.046875 C 1.171875 -4.75 1.578125 -5.546875 2.421875 -5.546875 C 3.25 -5.546875 3.671875 -4.734375 3.671875 -3.0625 Z M 3.671875 -3.0625 "/>
</g>
<g id="glyph-0-35">
<path d="M 4.5 -4.40625 C 4.5 -5.46875 3.671875 -6.234375 2.5 -6.234375 C 1.21875 -6.234375 0.484375 -5.59375 0.4375 -4.078125 L 1.21875 -4.078125 C 1.28125 -5.125 1.703125 -5.5625 2.46875 -5.5625 C 3.171875 -5.5625 3.703125 -5.0625 3.703125 -4.390625 C 3.703125 -3.890625 3.40625 -3.46875 2.859375 -3.15625 L 2.046875 -2.703125 C 0.75 -1.96875 0.375 -1.375 0.296875 0 L 4.453125 0 L 4.453125 -0.765625 L 1.171875 -0.765625 C 1.25 -1.28125 1.53125 -1.59375 2.296875 -2.046875 L 3.171875 -2.53125 C 4.046875 -2.984375 4.5 -3.640625 4.5 -4.40625 Z M 4.5 -4.40625 "/>
</g>
<g id="glyph-0-36">
<path d="M 4.515625 -2.0625 C 4.515625 -3.296875 3.6875 -4.109375 2.5 -4.109375 C 2.0625 -4.109375 1.703125 -4 1.34375 -3.734375 L 1.59375 -5.34375 L 4.1875 -5.34375 L 4.1875 -6.109375 L 0.96875 -6.109375 L 0.5 -2.84375 L 1.21875 -2.84375 C 1.578125 -3.265625 1.875 -3.421875 2.359375 -3.421875 C 3.1875 -3.421875 3.71875 -2.890625 3.71875 -1.96875 C 3.71875 -1.0625 3.203125 -0.546875 2.359375 -0.546875 C 1.6875 -0.546875 1.265625 -0.890625 1.078125 -1.59375 L 0.3125 -1.59375 C 0.5625 -0.359375 1.265625 0.125 2.375 0.125 C 3.640625 0.125 4.515625 -0.75 4.515625 -2.0625 Z M 4.515625 -2.0625 "/>
</g>
<g id="glyph-0-37">
<path d="M 4.578125 -5.453125 L 4.578125 -6.109375 L 0.40625 -6.109375 L 0.40625 -5.34375 L 3.78125 -5.34375 C 2.53125 -3.78125 1.65625 -1.953125 1.21875 0 L 2.046875 0 C 2.390625 -2.015625 3.265625 -3.890625 4.578125 -5.453125 Z M 4.578125 -5.453125 "/>
</g>
<g id="glyph-0-38">
<path d="M 3.046875 0 L 3.046875 -6.234375 L 2.546875 -6.234375 C 2.265625 -5.28125 2.09375 -5.140625 0.890625 -5 L 0.890625 -4.4375 L 2.28125 -4.4375 L 2.28125 0 Z M 3.046875 0 "/>
</g>
<g id="glyph-1-0">
<path d="M 5.25 -1.984375 L 4.328125 -1.984375 C 4.171875 -1.0625 3.703125 -0.6875 2.921875 -0.6875 C 1.90625 -0.6875 1.296875 -1.46875 1.296875 -2.828125 C 1.296875 -4.265625 1.890625 -5.078125 2.890625 -5.078125 C 3.65625 -5.078125 4.140625 -4.625 4.25 -3.828125 L 5.1875 -3.828125 C 5.078125 -5.234375 4.1875 -5.921875 2.90625 -5.921875 C 1.359375 -5.921875 0.34375 -4.734375 0.34375 -2.828125 C 0.34375 -0.96875 1.328125 0.171875 2.890625 0.171875 C 4.265625 0.171875 5.140625 -0.65625 5.25 -1.984375 Z M 5.25 -1.984375 "/>
</g>
<g id="glyph-1-1">
<path d="M 5.609375 -2.84375 C 5.609375 -4.828125 4.65625 -5.921875 2.984375 -5.921875 C 1.375 -5.921875 0.390625 -4.8125 0.390625 -2.875 C 0.390625 -0.953125 1.359375 0.171875 3 0.171875 C 4.625 0.171875 5.609375 -0.953125 5.609375 -2.84375 Z M 4.65625 -2.84375 C 4.65625 -1.5 4.015625 -0.6875 3 -0.6875 C 1.984375 -0.6875 1.359375 -1.484375 1.359375 -2.875 C 1.359375 -4.265625 1.984375 -5.078125 3 -5.078125 C 4.03125 -5.078125 4.65625 -4.28125 4.65625 -2.84375 Z M 4.65625 -2.84375 "/>
</g>
<g id="glyph-1-2">
<path d="M 8.359375 0 L 8.359375 -4.328125 C 8.359375 -5.359375 7.78125 -5.921875 6.703125 -5.921875 C 5.9375 -5.921875 5.484375 -5.703125 4.9375 -5.046875 C 4.59375 -5.671875 4.140625 -5.921875 3.390625 -5.921875 C 2.625 -5.921875 2.109375 -5.640625 1.609375 -4.953125 L 1.609375 -5.765625 L 0.78125 -5.765625 L 0.78125 0 L 1.6875 0 L 1.6875 -3.625 C 1.6875 -4.453125 2.296875 -5.125 3.046875 -5.125 C 3.734375 -5.125 4.109375 -4.703125 4.109375 -3.96875 L 4.109375 0 L 5.03125 0 L 5.03125 -3.625 C 5.03125 -4.453125 5.640625 -5.125 6.390625 -5.125 C 7.0625 -5.125 7.453125 -4.703125 7.453125 -3.96875 L 7.453125 0 Z M 8.359375 0 "/>
</g>
<g id="glyph-1-3">
<path d="M 5.890625 -0.015625 L 5.890625 -0.71875 C 5.78125 -0.6875 5.734375 -0.6875 5.6875 -0.6875 C 5.375 -0.6875 5.1875 -0.859375 5.1875 -1.140625 L 5.1875 -4.359375 C 5.1875 -5.375 4.4375 -5.921875 3.03125 -5.921875 C 1.625 -5.921875 0.765625 -5.390625 0.71875 -4.0625 L 1.640625 -4.0625 C 1.71875 -4.765625 2.140625 -5.078125 2.984375 -5.078125 C 3.8125 -5.078125 4.28125 -4.78125 4.28125 -4.21875 L 4.28125 -3.984375 C 4.28125 -3.59375 4.046875 -3.4375 3.328125 -3.34375 C 2.03125 -3.171875 1.828125 -3.140625 1.46875 -2.984375 C 0.796875 -2.71875 0.46875 -2.203125 0.46875 -1.5 C 0.46875 -0.453125 1.1875 0.171875 2.359375 0.171875 C 3.09375 0.171875 3.8125 -0.140625 4.3125 -0.6875 C 4.40625 -0.234375 4.8125 0.078125 5.265625 0.078125 C 5.4375 0.078125 5.59375 0.0625 5.890625 -0.015625 Z M 4.28125 -1.984375 C 4.28125 -1.171875 3.4375 -0.640625 2.546875 -0.640625 C 1.84375 -0.640625 1.421875 -0.890625 1.421875 -1.515625 C 1.421875 -2.125 1.828125 -2.390625 2.8125 -2.53125 C 3.765625 -2.65625 3.96875 -2.703125 4.28125 -2.84375 Z M 4.28125 -1.984375 "/>
</g>
<g id="glyph-1-4">
<path d="M 5.359375 0 L 5.359375 -4.359375 C 5.359375 -5.3125 4.640625 -5.921875 3.53125 -5.921875 C 2.671875 -5.921875 2.125 -5.59375 1.609375 -4.796875 L 1.609375 -5.765625 L 0.765625 -5.765625 L 0.765625 0 L 1.6875 0 L 1.6875 -3.171875 C 1.6875 -4.359375 2.328125 -5.125 3.25 -5.125 C 3.984375 -5.125 4.4375 -4.6875 4.4375 -4 L 4.4375 0 Z M 5.359375 0 "/>
</g>
<g id="glyph-1-5">
<path d="M 5.4375 0 L 5.4375 -8.015625 L 4.53125 -8.015625 L 4.53125 -5.03125 C 4.140625 -5.625 3.53125 -5.921875 2.765625 -5.921875 C 1.265625 -5.921875 0.28125 -4.78125 0.28125 -2.9375 C 0.28125 -0.984375 1.25 0.171875 2.796875 0.171875 C 3.59375 0.171875 4.140625 -0.125 4.625 -0.84375 L 4.625 0 Z M 4.53125 -2.859375 C 4.53125 -1.53125 3.890625 -0.6875 2.921875 -0.6875 C 1.90625 -0.6875 1.25 -1.546875 1.25 -2.875 C 1.25 -4.21875 1.90625 -5.078125 2.921875 -5.078125 C 3.90625 -5.078125 4.53125 -4.1875 4.53125 -2.859375 Z M 4.53125 -2.859375 "/>
</g>
<g id="glyph-2-0">
<path d="M -1.984375 -5.25 L -1.984375 -4.328125 C -1.0625 -4.171875 -0.6875 -3.703125 -0.6875 -2.921875 C -0.6875 -1.90625 -1.46875 -1.296875 -2.828125 -1.296875 C -4.265625 -1.296875 -5.078125 -1.890625 -5.078125 -2.890625 C -5.078125 -3.65625 -4.625 -4.140625 -3.828125 -4.25 L -3.828125 -5.1875 C -5.234375 -5.078125 -5.921875 -4.1875 -5.921875 -2.90625 C -5.921875 -1.359375 -4.734375 -0.34375 -2.828125 -0.34375 C -0.96875 -0.34375 0.171875 -1.328125 0.171875 -2.890625 C 0.171875 -4.265625 -0.65625 -5.140625 -1.984375 -5.25 Z M -1.984375 -5.25 "/>
</g>
<g id="glyph-2-1">
<path d="M -2.84375 -5.609375 C -4.828125 -5.609375 -5.921875 -4.65625 -5.921875 -2.984375 C -5.921875 -1.375 -4.8125 -0.390625 -2.875 -0.390625 C -0.953125 -0.390625 0.171875 -1.359375 0.171875 -3 C 0.171875 -4.625 -0.953125 -5.609375 -2.84375 -5.609375 Z M -2.84375 -4.65625 C -1.5 -4.65625 -0.6875 -4.015625 -0.6875 -3 C -0.6875 -1.984375 -1.484375 -1.359375 -2.875 -1.359375 C -4.265625 -1.359375 -5.078125 -1.984375 -5.078125 -3 C -5.078125 -4.03125 -4.28125 -4.65625 -2.84375 -4.65625 Z M -2.84375 -4.65625 "/>
</g>
<g id="glyph-2-2">
<path d="M 0 -5.296875 L -5.765625 -5.296875 L -5.765625 -4.390625 L -2.578125 -4.390625 C -1.40625 -4.390625 -0.640625 -3.765625 -0.640625 -2.8125 C -0.640625 -2.09375 -1.078125 -1.625 -1.765625 -1.625 L -5.765625 -1.625 L -5.765625 -0.71875 L -1.40625 -0.71875 C -0.453125 -0.71875 0.171875 -1.4375 0.171875 -2.546875 C 0.171875 -3.40625 -0.125 -3.9375 -0.890625 -4.484375 L 0 -4.484375 Z M 0 -5.296875 "/>
</g>
<g id="glyph-2-3">
<path d="M 0 -5.359375 L -4.359375 -5.359375 C -5.3125 -5.359375 -5.921875 -4.640625 -5.921875 -3.53125 C -5.921875 -2.671875 -5.59375 -2.125 -4.796875 -1.609375 L -5.765625 -1.609375 L -5.765625 -0.765625 L 0 -0.765625 L 0 -1.6875 L -3.171875 -1.6875 C -4.359375 -1.6875 -5.125 -2.328125 -5.125 -3.25 C -5.125 -3.984375 -4.6875 -4.4375 -4 -4.4375 L 0 -4.4375 Z M 0 -5.359375 "/>
</g>
<g id="glyph-2-4">
<path d="M 0 -2.796875 L -0.765625 -2.796875 C -0.734375 -2.671875 -0.71875 -2.53125 -0.71875 -2.359375 C -0.71875 -1.953125 -0.84375 -1.84375 -1.25 -1.84375 L -5.015625 -1.84375 L -5.015625 -2.796875 L -5.765625 -2.796875 L -5.765625 -1.84375 L -7.34375 -1.84375 L -7.34375 -0.9375 L -5.765625 -0.9375 L -5.765625 -0.15625 L -5.015625 -0.15625 L -5.015625 -0.9375 L -0.84375 -0.9375 C -0.25 -0.9375 0.078125 -1.328125 0.078125 -2.046875 C 0.078125 -2.265625 0.0625 -2.484375 0 -2.796875 Z M 0 -2.796875 "/>
</g>
</g>
<clipPath id="clip-0">
<path clip-rule="nonzero" d="M 46.152344 23.246094 L 1043.523438 23.246094 L 1043.523438 200.957031 L 46.152344 200.957031 Z M 46.152344 23.246094 "/>
</clipPath>
<clipPath id="clip-1">
<path clip-rule="nonzero" d="M 46.152344 238.136719 L 1043.523438 238.136719 L 1043.523438 415.847656 L 46.152344 415.847656 Z M 46.152344 238.136719 "/>
</clipPath>
<clipPath id="clip-2">
<path clip-rule="nonzero" d="M 46.152344 220.371094 L 1043.523438 220.371094 L 1043.523438 238.136719 L 46.152344 238.136719 Z M 46.152344 220.371094 "/>
</clipPath>
<clipPath id="clip-3">
<path clip-rule="nonzero" d="M 46.152344 5.480469 L 1043.523438 5.480469 L 1043.523438 23.246094 L 46.152344 23.246094 Z M 46.152344 5.480469 "/>
</clipPath>
</defs>
<rect x="-104.9" y="-45" width="1258.8" height="540" fill="rgb(100%, 100%, 100%)" fill-opacity="1"/>
<rect x="-104.9" y="-45" width="1258.8" height="540" fill="rgb(100%, 100%, 100%)" fill-opacity="1"/>
<path fill="none" stroke-width="1.066978" stroke-linecap="round" stroke-linejoin="round" stroke="rgb(100%, 100%, 100%)" stroke-opacity="1" stroke-miterlimit="10" d="M 0 450 L 1049 450 L 1049 0 L 0 0 Z M 0 450 "/>
<g clip-path="url(#clip-0)">
<path fill-rule="nonzero" fill="rgb(100%, 100%, 100%)" fill-opacity="1" d="M 46.152344 200.957031 L 1043.523438 200.957031 L 1043.523438 23.246094 L 46.152344 23.246094 Z M 46.152344 200.957031 "/>
</g>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 52.890625 192.882812 L 93.324219 192.882812 L 93.324219 192.066406 L 52.890625 192.066406 Z M 52.890625 192.882812 "/>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 97.816406 192.882812 L 138.25 192.882812 L 138.25 188.390625 L 97.816406 188.390625 Z M 97.816406 192.882812 "/>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 142.742188 192.882812 L 183.175781 192.882812 L 183.175781 179.816406 L 142.742188 179.816406 Z M 142.742188 192.882812 "/>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 187.667969 192.882812 L 228.101562 192.882812 L 228.101562 192.066406 L 187.667969 192.066406 Z M 187.667969 192.882812 "/>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 232.597656 192.882812 L 273.03125 192.882812 L 273.03125 192.472656 L 232.597656 192.472656 Z M 232.597656 192.882812 "/>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 277.523438 192.882812 L 317.957031 192.882812 L 317.957031 191.65625 L 277.523438 191.65625 Z M 277.523438 192.882812 "/>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 322.449219 192.882812 L 362.882812 192.882812 L 362.882812 189.34375 L 322.449219 189.34375 Z M 322.449219 192.882812 "/>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 367.375 192.882812 L 407.808594 192.882812 L 407.808594 192.746094 L 367.375 192.746094 Z M 367.375 192.882812 "/>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 412.300781 192.882812 L 452.734375 192.882812 L 452.734375 31.324219 L 412.300781 31.324219 Z M 412.300781 192.882812 "/>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 457.230469 192.882812 L 497.664062 192.882812 L 497.664062 159.128906 L 457.230469 159.128906 Z M 457.230469 192.882812 "/>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 502.15625 192.882812 L 542.589844 192.882812 L 542.589844 174.917969 L 502.15625 174.917969 Z M 502.15625 192.882812 "/>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 547.082031 192.882812 L 587.515625 192.882812 L 587.515625 159.671875 L 547.082031 159.671875 Z M 547.082031 192.882812 "/>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 592.007812 192.882812 L 632.441406 192.882812 L 632.441406 169.882812 L 592.007812 169.882812 Z M 592.007812 192.882812 "/>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 636.933594 192.882812 L 677.367188 192.882812 L 677.367188 190.976562 L 636.933594 190.976562 Z M 636.933594 192.882812 "/>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 681.863281 192.882812 L 722.296875 192.882812 L 722.296875 192.609375 L 681.863281 192.609375 Z M 681.863281 192.882812 "/>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 726.789062 192.882812 L 767.222656 192.882812 L 767.222656 152.730469 L 726.789062 152.730469 Z M 726.789062 192.882812 "/>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 771.714844 192.882812 L 812.148438 192.882812 L 812.148438 103.324219 L 771.714844 103.324219 Z M 771.714844 192.882812 "/>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 816.640625 192.882812 L 857.074219 192.882812 L 857.074219 91.347656 L 816.640625 91.347656 Z M 816.640625 192.882812 "/>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 861.566406 192.882812 L 902 192.882812 L 902 165.253906 L 861.566406 165.253906 Z M 861.566406 192.882812 "/>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 906.496094 192.882812 L 946.929688 192.882812 L 946.929688 189.753906 L 906.496094 189.753906 Z M 906.496094 192.882812 "/>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 951.421875 192.882812 L 991.855469 192.882812 L 991.855469 192.472656 L 951.421875 192.472656 Z M 951.421875 192.882812 "/>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 996.347656 192.882812 L 1036.78125 192.882812 L 1036.78125 122.789062 L 996.347656 122.789062 Z M 996.347656 192.882812 "/>
<g clip-path="url(#clip-1)">
<path fill-rule="nonzero" fill="rgb(100%, 100%, 100%)" fill-opacity="1" d="M 46.152344 415.847656 L 1043.523438 415.847656 L 1043.523438 238.136719 L 46.152344 238.136719 Z M 46.152344 415.847656 "/>
</g>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 52.890625 407.769531 L 93.324219 407.769531 L 93.324219 400.195312 L 52.890625 400.195312 Z M 52.890625 407.769531 "/>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 97.816406 407.769531 L 138.25 407.769531 L 138.25 406.507812 L 97.816406 406.507812 Z M 97.816406 407.769531 "/>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 142.742188 407.769531 L 183.175781 407.769531 L 183.175781 406.507812 L 142.742188 406.507812 Z M 142.742188 407.769531 "/>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 187.667969 407.769531 L 228.101562 407.769531 L 228.101562 406.507812 L 187.667969 406.507812 Z M 187.667969 407.769531 "/>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 232.597656 407.769531 L 273.03125 407.769531 L 273.03125 403.984375 L 232.597656 403.984375 Z M 232.597656 407.769531 "/>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 277.523438 407.769531 L 317.957031 407.769531 L 317.957031 405.246094 L 277.523438 405.246094 Z M 277.523438 407.769531 "/>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 322.449219 407.769531 L 362.882812 407.769531 L 362.882812 406.507812 L 322.449219 406.507812 Z M 322.449219 407.769531 "/>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 367.375 407.769531 L 407.808594 407.769531 L 407.808594 406.507812 L 367.375 406.507812 Z M 367.375 407.769531 "/>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 412.300781 407.769531 L 452.734375 407.769531 L 452.734375 246.210938 L 412.300781 246.210938 Z M 412.300781 407.769531 "/>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 457.230469 407.769531 L 497.664062 407.769531 L 497.664062 406.507812 L 457.230469 406.507812 Z M 457.230469 407.769531 "/>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 502.15625 407.769531 L 542.589844 407.769531 L 542.589844 392.625 L 502.15625 392.625 Z M 502.15625 407.769531 "/>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 547.082031 407.769531 L 587.515625 407.769531 L 587.515625 396.410156 L 547.082031 396.410156 Z M 547.082031 407.769531 "/>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 592.007812 407.769531 L 632.441406 407.769531 L 632.441406 406.507812 L 592.007812 406.507812 Z M 592.007812 407.769531 "/>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 636.933594 407.769531 L 677.367188 407.769531 L 677.367188 406.507812 L 636.933594 406.507812 Z M 636.933594 407.769531 "/>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 681.863281 407.769531 L 722.296875 407.769531 L 722.296875 405.246094 L 681.863281 405.246094 Z M 681.863281 407.769531 "/>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 726.789062 407.769531 L 767.222656 407.769531 L 767.222656 406.507812 L 726.789062 406.507812 Z M 726.789062 407.769531 "/>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 771.714844 407.769531 L 812.148438 407.769531 L 812.148438 390.097656 L 771.714844 390.097656 Z M 771.714844 407.769531 "/>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 816.640625 407.769531 L 857.074219 407.769531 L 857.074219 385.050781 L 816.640625 385.050781 Z M 816.640625 407.769531 "/>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 861.566406 407.769531 L 902 407.769531 L 902 380.003906 L 861.566406 380.003906 Z M 861.566406 407.769531 "/>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 906.496094 407.769531 L 946.929688 407.769531 L 946.929688 383.789062 L 906.496094 383.789062 Z M 906.496094 407.769531 "/>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 951.421875 407.769531 L 991.855469 407.769531 L 991.855469 403.984375 L 951.421875 403.984375 Z M 951.421875 407.769531 "/>
<path fill-rule="nonzero" fill="rgb(34.901961%, 34.901961%, 34.901961%)" fill-opacity="1" d="M 996.347656 407.769531 L 1036.78125 407.769531 L 1036.78125 406.507812 L 996.347656 406.507812 Z M 996.347656 407.769531 "/>
<g clip-path="url(#clip-2)">
<path fill-rule="nonzero" fill="rgb(100%, 100%, 100%)" fill-opacity="1" stroke-width="2.133957" stroke-linecap="round" stroke-linejoin="round" stroke="rgb(0%, 0%, 0%)" stroke-opacity="1" stroke-miterlimit="10" d="M 46.152344 238.136719 L 1043.523438 238.136719 L 1043.523438 220.371094 L 46.152344 220.371094 Z M 46.152344 238.136719 "/>
</g>
<g fill="rgb(10.196078%, 10.196078%, 10.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-0" x="530.835938" y="231.856445"/>
<use xlink:href="#glyph-0-1" x="536.835938" y="231.856445"/>
<use xlink:href="#glyph-0-2" x="541.835938" y="231.856445"/>
<use xlink:href="#glyph-0-3" x="543.835938" y="231.856445"/>
<use xlink:href="#glyph-0-4" x="548.835938" y="231.856445"/>
<use xlink:href="#glyph-0-5" x="553.835938" y="231.856445"/>
</g>
<g clip-path="url(#clip-3)">
<path fill-rule="nonzero" fill="rgb(100%, 100%, 100%)" fill-opacity="1" stroke-width="2.133957" stroke-linecap="round" stroke-linejoin="round" stroke="rgb(0%, 0%, 0%)" stroke-opacity="1" stroke-miterlimit="10" d="M 46.152344 23.246094 L 1043.523438 23.246094 L 1043.523438 5.480469 L 46.152344 5.480469 Z M 46.152344 23.246094 "/>
</g>
<g fill="rgb(10.196078%, 10.196078%, 10.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-6" x="536.335938" y="16.96582"/>
<use xlink:href="#glyph-0-7" x="542.335938" y="16.96582"/>
<use xlink:href="#glyph-0-8" x="547.335938" y="16.96582"/>
</g>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(0%, 0%, 0%)" stroke-opacity="1" stroke-miterlimit="10" d="M 46.152344 415.847656 L 1043.519531 415.847656 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 73.105469 418.589844 L 73.105469 415.847656 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 118.03125 418.589844 L 118.03125 415.847656 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 162.960938 418.589844 L 162.960938 415.847656 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 207.886719 418.589844 L 207.886719 415.847656 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 252.8125 418.589844 L 252.8125 415.847656 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 297.738281 418.589844 L 297.738281 415.847656 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 342.667969 418.589844 L 342.667969 415.847656 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 387.59375 418.589844 L 387.59375 415.847656 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 432.519531 418.589844 L 432.519531 415.847656 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 477.445312 418.589844 L 477.445312 415.847656 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 522.371094 418.589844 L 522.371094 415.847656 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 567.300781 418.589844 L 567.300781 415.847656 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 612.226562 418.589844 L 612.226562 415.847656 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 657.152344 418.589844 L 657.152344 415.847656 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 702.078125 418.589844 L 702.078125 415.847656 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 747.003906 418.589844 L 747.003906 415.847656 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 791.933594 418.589844 L 791.933594 415.847656 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 836.859375 418.589844 L 836.859375 415.847656 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 881.785156 418.589844 L 881.785156 415.847656 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 926.710938 418.589844 L 926.710938 415.847656 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 971.636719 418.589844 L 971.636719 415.847656 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 1016.566406 418.589844 L 1016.566406 415.847656 "/>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-9" x="57.605469" y="426.883789"/>
<use xlink:href="#glyph-0-10" x="63.605469" y="426.883789"/>
<use xlink:href="#glyph-0-10" x="68.605469" y="426.883789"/>
<use xlink:href="#glyph-0-5" x="73.605469" y="426.883789"/>
<use xlink:href="#glyph-0-1" x="78.605469" y="426.883789"/>
<use xlink:href="#glyph-0-11" x="83.605469" y="426.883789"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-12" x="99.03125" y="426.883789"/>
<use xlink:href="#glyph-0-7" x="105.03125" y="426.883789"/>
<use xlink:href="#glyph-0-10" x="110.03125" y="426.883789"/>
<use xlink:href="#glyph-0-7" x="115.03125" y="426.883789"/>
<use xlink:href="#glyph-0-13" x="120.03125" y="426.883789"/>
<use xlink:href="#glyph-0-2" x="125.03125" y="426.883789"/>
<use xlink:href="#glyph-0-14" x="127.03125" y="426.883789"/>
<use xlink:href="#glyph-0-2" x="129.03125" y="426.883789"/>
<use xlink:href="#glyph-0-15" x="131.03125" y="426.883789"/>
<use xlink:href="#glyph-0-16" x="133.03125" y="426.883789"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-12" x="150.960938" y="426.883789"/>
<use xlink:href="#glyph-0-17" x="156.960938" y="426.883789"/>
<use xlink:href="#glyph-0-5" x="161.960938" y="426.883789"/>
<use xlink:href="#glyph-0-18" x="166.960938" y="426.883789"/>
<use xlink:href="#glyph-0-19" x="170.960938" y="426.883789"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-12" x="196.886719" y="426.883789"/>
<use xlink:href="#glyph-0-14" x="202.886719" y="426.883789"/>
<use xlink:href="#glyph-0-20" x="204.886719" y="426.883789"/>
<use xlink:href="#glyph-0-21" x="209.886719" y="426.883789"/>
<use xlink:href="#glyph-0-5" x="213.886719" y="426.883789"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-12" x="239.8125" y="426.883789"/>
<use xlink:href="#glyph-0-22" x="245.8125" y="426.883789"/>
<use xlink:href="#glyph-0-5" x="248.8125" y="426.883789"/>
<use xlink:href="#glyph-0-7" x="253.8125" y="426.883789"/>
<use xlink:href="#glyph-0-15" x="258.8125" y="426.883789"/>
<use xlink:href="#glyph-0-5" x="260.8125" y="426.883789"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-23" x="283.738281" y="426.883789"/>
<use xlink:href="#glyph-0-1" x="289.738281" y="426.883789"/>
<use xlink:href="#glyph-0-7" x="294.738281" y="426.883789"/>
<use xlink:href="#glyph-0-13" x="299.738281" y="426.883789"/>
<use xlink:href="#glyph-0-14" x="304.738281" y="426.883789"/>
<use xlink:href="#glyph-0-5" x="306.738281" y="426.883789"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-23" x="325.667969" y="426.883789"/>
<use xlink:href="#glyph-0-24" x="331.667969" y="426.883789"/>
<use xlink:href="#glyph-0-7" x="335.667969" y="426.883789"/>
<use xlink:href="#glyph-0-25" x="340.667969" y="426.883789"/>
<use xlink:href="#glyph-0-2" x="347.667969" y="426.883789"/>
<use xlink:href="#glyph-0-1" x="349.667969" y="426.883789"/>
<use xlink:href="#glyph-0-5" x="354.667969" y="426.883789"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-23" x="370.09375" y="426.883789"/>
<use xlink:href="#glyph-0-24" x="376.09375" y="426.883789"/>
<use xlink:href="#glyph-0-10" x="380.09375" y="426.883789"/>
<use xlink:href="#glyph-0-4" x="385.09375" y="426.883789"/>
<use xlink:href="#glyph-0-1" x="390.09375" y="426.883789"/>
<use xlink:href="#glyph-0-26" x="395.09375" y="426.883789"/>
<use xlink:href="#glyph-0-5" x="400.09375" y="426.883789"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-27" x="422.019531" y="426.883789"/>
<use xlink:href="#glyph-0-5" x="427.019531" y="426.883789"/>
<use xlink:href="#glyph-0-15" x="432.019531" y="426.883789"/>
<use xlink:href="#glyph-0-18" x="434.019531" y="426.883789"/>
<use xlink:href="#glyph-0-17" x="438.019531" y="426.883789"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-28" x="470.445312" y="426.883789"/>
<use xlink:href="#glyph-0-11" x="472.445312" y="426.883789"/>
<use xlink:href="#glyph-0-14" x="477.445312" y="426.883789"/>
<use xlink:href="#glyph-0-5" x="479.445312" y="426.883789"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-29" x="515.871094" y="426.883789"/>
<use xlink:href="#glyph-0-2" x="520.871094" y="426.883789"/>
<use xlink:href="#glyph-0-21" x="522.871094" y="426.883789"/>
<use xlink:href="#glyph-0-15" x="526.871094" y="426.883789"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-29" x="556.300781" y="426.883789"/>
<use xlink:href="#glyph-0-20" x="561.300781" y="426.883789"/>
<use xlink:href="#glyph-0-26" x="566.300781" y="426.883789"/>
<use xlink:href="#glyph-0-2" x="571.300781" y="426.883789"/>
<use xlink:href="#glyph-0-1" x="573.300781" y="426.883789"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-29" x="598.726562" y="426.883789"/>
<use xlink:href="#glyph-0-20" x="603.726562" y="426.883789"/>
<use xlink:href="#glyph-0-26" x="608.726562" y="426.883789"/>
<use xlink:href="#glyph-0-20" x="613.726562" y="426.883789"/>
<use xlink:href="#glyph-0-4" x="618.726562" y="426.883789"/>
<use xlink:href="#glyph-0-15" x="623.726562" y="426.883789"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-29" x="647.652344" y="426.883789"/>
<use xlink:href="#glyph-0-21" x="652.652344" y="426.883789"/>
<use xlink:href="#glyph-0-4" x="656.652344" y="426.883789"/>
<use xlink:href="#glyph-0-13" x="661.652344" y="426.883789"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-30" x="691.578125" y="426.883789"/>
<use xlink:href="#glyph-0-20" x="698.578125" y="426.883789"/>
<use xlink:href="#glyph-0-31" x="703.578125" y="426.883789"/>
<use xlink:href="#glyph-0-5" x="707.578125" y="426.883789"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-32" x="736.503906" y="426.883789"/>
<use xlink:href="#glyph-0-20" x="742.503906" y="426.883789"/>
<use xlink:href="#glyph-0-20" x="747.503906" y="426.883789"/>
<use xlink:href="#glyph-0-10" x="752.503906" y="426.883789"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-33" x="777.933594" y="426.883789"/>
<use xlink:href="#glyph-0-5" x="783.933594" y="426.883789"/>
<use xlink:href="#glyph-0-7" x="788.933594" y="426.883789"/>
<use xlink:href="#glyph-0-22" x="793.933594" y="426.883789"/>
<use xlink:href="#glyph-0-18" x="796.933594" y="426.883789"/>
<use xlink:href="#glyph-0-17" x="800.933594" y="426.883789"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-33" x="824.859375" y="426.883789"/>
<use xlink:href="#glyph-0-5" x="830.859375" y="426.883789"/>
<use xlink:href="#glyph-0-14" x="835.859375" y="426.883789"/>
<use xlink:href="#glyph-0-5" x="837.859375" y="426.883789"/>
<use xlink:href="#glyph-0-18" x="842.859375" y="426.883789"/>
<use xlink:href="#glyph-0-15" x="846.859375" y="426.883789"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-33" x="869.785156" y="426.883789"/>
<use xlink:href="#glyph-0-15" x="875.785156" y="426.883789"/>
<use xlink:href="#glyph-0-7" x="877.785156" y="426.883789"/>
<use xlink:href="#glyph-0-15" x="882.785156" y="426.883789"/>
<use xlink:href="#glyph-0-4" x="884.785156" y="426.883789"/>
<use xlink:href="#glyph-0-21" x="889.785156" y="426.883789"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-33" x="916.210938" y="426.883789"/>
<use xlink:href="#glyph-0-15" x="922.210938" y="426.883789"/>
<use xlink:href="#glyph-0-20" x="924.210938" y="426.883789"/>
<use xlink:href="#glyph-0-22" x="929.210938" y="426.883789"/>
<use xlink:href="#glyph-0-5" x="932.210938" y="426.883789"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-33" x="952.136719" y="426.883789"/>
<use xlink:href="#glyph-0-4" x="958.136719" y="426.883789"/>
<use xlink:href="#glyph-0-13" x="963.136719" y="426.883789"/>
<use xlink:href="#glyph-0-21" x="968.136719" y="426.883789"/>
<use xlink:href="#glyph-0-18" x="972.136719" y="426.883789"/>
<use xlink:href="#glyph-0-22" x="976.136719" y="426.883789"/>
<use xlink:href="#glyph-0-2" x="979.136719" y="426.883789"/>
<use xlink:href="#glyph-0-13" x="981.136719" y="426.883789"/>
<use xlink:href="#glyph-0-5" x="986.136719" y="426.883789"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-0" x="1000.066406" y="426.883789"/>
<use xlink:href="#glyph-0-1" x="1006.066406" y="426.883789"/>
<use xlink:href="#glyph-0-21" x="1011.066406" y="426.883789"/>
<use xlink:href="#glyph-0-5" x="1015.066406" y="426.883789"/>
<use xlink:href="#glyph-0-14" x="1020.066406" y="426.883789"/>
<use xlink:href="#glyph-0-5" x="1022.066406" y="426.883789"/>
<use xlink:href="#glyph-0-18" x="1027.066406" y="426.883789"/>
<use xlink:href="#glyph-0-15" x="1031.066406" y="426.883789"/>
</g>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(0%, 0%, 0%)" stroke-opacity="1" stroke-miterlimit="10" d="M 46.152344 200.957031 L 1043.519531 200.957031 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 73.105469 203.699219 L 73.105469 200.957031 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 118.03125 203.699219 L 118.03125 200.957031 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 162.960938 203.699219 L 162.960938 200.957031 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 207.886719 203.699219 L 207.886719 200.957031 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 252.8125 203.699219 L 252.8125 200.957031 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 297.738281 203.699219 L 297.738281 200.957031 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 342.667969 203.699219 L 342.667969 200.957031 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 387.59375 203.699219 L 387.59375 200.957031 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 432.519531 203.699219 L 432.519531 200.957031 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 477.445312 203.699219 L 477.445312 200.957031 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 522.371094 203.699219 L 522.371094 200.957031 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 567.300781 203.699219 L 567.300781 200.957031 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 612.226562 203.699219 L 612.226562 200.957031 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 657.152344 203.699219 L 657.152344 200.957031 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 702.078125 203.699219 L 702.078125 200.957031 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 747.003906 203.699219 L 747.003906 200.957031 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 791.933594 203.699219 L 791.933594 200.957031 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 836.859375 203.699219 L 836.859375 200.957031 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 881.785156 203.699219 L 881.785156 200.957031 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 926.710938 203.699219 L 926.710938 200.957031 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 971.636719 203.699219 L 971.636719 200.957031 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 1016.566406 203.699219 L 1016.566406 200.957031 "/>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-9" x="57.605469" y="211.993164"/>
<use xlink:href="#glyph-0-10" x="63.605469" y="211.993164"/>
<use xlink:href="#glyph-0-10" x="68.605469" y="211.993164"/>
<use xlink:href="#glyph-0-5" x="73.605469" y="211.993164"/>
<use xlink:href="#glyph-0-1" x="78.605469" y="211.993164"/>
<use xlink:href="#glyph-0-11" x="83.605469" y="211.993164"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-12" x="99.03125" y="211.993164"/>
<use xlink:href="#glyph-0-7" x="105.03125" y="211.993164"/>
<use xlink:href="#glyph-0-10" x="110.03125" y="211.993164"/>
<use xlink:href="#glyph-0-7" x="115.03125" y="211.993164"/>
<use xlink:href="#glyph-0-13" x="120.03125" y="211.993164"/>
<use xlink:href="#glyph-0-2" x="125.03125" y="211.993164"/>
<use xlink:href="#glyph-0-14" x="127.03125" y="211.993164"/>
<use xlink:href="#glyph-0-2" x="129.03125" y="211.993164"/>
<use xlink:href="#glyph-0-15" x="131.03125" y="211.993164"/>
<use xlink:href="#glyph-0-16" x="133.03125" y="211.993164"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-12" x="150.960938" y="211.993164"/>
<use xlink:href="#glyph-0-17" x="156.960938" y="211.993164"/>
<use xlink:href="#glyph-0-5" x="161.960938" y="211.993164"/>
<use xlink:href="#glyph-0-18" x="166.960938" y="211.993164"/>
<use xlink:href="#glyph-0-19" x="170.960938" y="211.993164"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-12" x="196.886719" y="211.993164"/>
<use xlink:href="#glyph-0-14" x="202.886719" y="211.993164"/>
<use xlink:href="#glyph-0-20" x="204.886719" y="211.993164"/>
<use xlink:href="#glyph-0-21" x="209.886719" y="211.993164"/>
<use xlink:href="#glyph-0-5" x="213.886719" y="211.993164"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-12" x="239.8125" y="211.993164"/>
<use xlink:href="#glyph-0-22" x="245.8125" y="211.993164"/>
<use xlink:href="#glyph-0-5" x="248.8125" y="211.993164"/>
<use xlink:href="#glyph-0-7" x="253.8125" y="211.993164"/>
<use xlink:href="#glyph-0-15" x="258.8125" y="211.993164"/>
<use xlink:href="#glyph-0-5" x="260.8125" y="211.993164"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-23" x="283.738281" y="211.993164"/>
<use xlink:href="#glyph-0-1" x="289.738281" y="211.993164"/>
<use xlink:href="#glyph-0-7" x="294.738281" y="211.993164"/>
<use xlink:href="#glyph-0-13" x="299.738281" y="211.993164"/>
<use xlink:href="#glyph-0-14" x="304.738281" y="211.993164"/>
<use xlink:href="#glyph-0-5" x="306.738281" y="211.993164"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-23" x="325.667969" y="211.993164"/>
<use xlink:href="#glyph-0-24" x="331.667969" y="211.993164"/>
<use xlink:href="#glyph-0-7" x="335.667969" y="211.993164"/>
<use xlink:href="#glyph-0-25" x="340.667969" y="211.993164"/>
<use xlink:href="#glyph-0-2" x="347.667969" y="211.993164"/>
<use xlink:href="#glyph-0-1" x="349.667969" y="211.993164"/>
<use xlink:href="#glyph-0-5" x="354.667969" y="211.993164"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-23" x="370.09375" y="211.993164"/>
<use xlink:href="#glyph-0-24" x="376.09375" y="211.993164"/>
<use xlink:href="#glyph-0-10" x="380.09375" y="211.993164"/>
<use xlink:href="#glyph-0-4" x="385.09375" y="211.993164"/>
<use xlink:href="#glyph-0-1" x="390.09375" y="211.993164"/>
<use xlink:href="#glyph-0-26" x="395.09375" y="211.993164"/>
<use xlink:href="#glyph-0-5" x="400.09375" y="211.993164"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-27" x="422.019531" y="211.993164"/>
<use xlink:href="#glyph-0-5" x="427.019531" y="211.993164"/>
<use xlink:href="#glyph-0-15" x="432.019531" y="211.993164"/>
<use xlink:href="#glyph-0-18" x="434.019531" y="211.993164"/>
<use xlink:href="#glyph-0-17" x="438.019531" y="211.993164"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-28" x="470.445312" y="211.993164"/>
<use xlink:href="#glyph-0-11" x="472.445312" y="211.993164"/>
<use xlink:href="#glyph-0-14" x="477.445312" y="211.993164"/>
<use xlink:href="#glyph-0-5" x="479.445312" y="211.993164"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-29" x="515.871094" y="211.993164"/>
<use xlink:href="#glyph-0-2" x="520.871094" y="211.993164"/>
<use xlink:href="#glyph-0-21" x="522.871094" y="211.993164"/>
<use xlink:href="#glyph-0-15" x="526.871094" y="211.993164"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-29" x="556.300781" y="211.993164"/>
<use xlink:href="#glyph-0-20" x="561.300781" y="211.993164"/>
<use xlink:href="#glyph-0-26" x="566.300781" y="211.993164"/>
<use xlink:href="#glyph-0-2" x="571.300781" y="211.993164"/>
<use xlink:href="#glyph-0-1" x="573.300781" y="211.993164"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-29" x="598.726562" y="211.993164"/>
<use xlink:href="#glyph-0-20" x="603.726562" y="211.993164"/>
<use xlink:href="#glyph-0-26" x="608.726562" y="211.993164"/>
<use xlink:href="#glyph-0-20" x="613.726562" y="211.993164"/>
<use xlink:href="#glyph-0-4" x="618.726562" y="211.993164"/>
<use xlink:href="#glyph-0-15" x="623.726562" y="211.993164"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-29" x="647.652344" y="211.993164"/>
<use xlink:href="#glyph-0-21" x="652.652344" y="211.993164"/>
<use xlink:href="#glyph-0-4" x="656.652344" y="211.993164"/>
<use xlink:href="#glyph-0-13" x="661.652344" y="211.993164"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-30" x="691.578125" y="211.993164"/>
<use xlink:href="#glyph-0-20" x="698.578125" y="211.993164"/>
<use xlink:href="#glyph-0-31" x="703.578125" y="211.993164"/>
<use xlink:href="#glyph-0-5" x="707.578125" y="211.993164"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-32" x="736.503906" y="211.993164"/>
<use xlink:href="#glyph-0-20" x="742.503906" y="211.993164"/>
<use xlink:href="#glyph-0-20" x="747.503906" y="211.993164"/>
<use xlink:href="#glyph-0-10" x="752.503906" y="211.993164"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-33" x="777.933594" y="211.993164"/>
<use xlink:href="#glyph-0-5" x="783.933594" y="211.993164"/>
<use xlink:href="#glyph-0-7" x="788.933594" y="211.993164"/>
<use xlink:href="#glyph-0-22" x="793.933594" y="211.993164"/>
<use xlink:href="#glyph-0-18" x="796.933594" y="211.993164"/>
<use xlink:href="#glyph-0-17" x="800.933594" y="211.993164"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-33" x="824.859375" y="211.993164"/>
<use xlink:href="#glyph-0-5" x="830.859375" y="211.993164"/>
<use xlink:href="#glyph-0-14" x="835.859375" y="211.993164"/>
<use xlink:href="#glyph-0-5" x="837.859375" y="211.993164"/>
<use xlink:href="#glyph-0-18" x="842.859375" y="211.993164"/>
<use xlink:href="#glyph-0-15" x="846.859375" y="211.993164"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-33" x="869.785156" y="211.993164"/>
<use xlink:href="#glyph-0-15" x="875.785156" y="211.993164"/>
<use xlink:href="#glyph-0-7" x="877.785156" y="211.993164"/>
<use xlink:href="#glyph-0-15" x="882.785156" y="211.993164"/>
<use xlink:href="#glyph-0-4" x="884.785156" y="211.993164"/>
<use xlink:href="#glyph-0-21" x="889.785156" y="211.993164"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-33" x="916.210938" y="211.993164"/>
<use xlink:href="#glyph-0-15" x="922.210938" y="211.993164"/>
<use xlink:href="#glyph-0-20" x="924.210938" y="211.993164"/>
<use xlink:href="#glyph-0-22" x="929.210938" y="211.993164"/>
<use xlink:href="#glyph-0-5" x="932.210938" y="211.993164"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-33" x="952.136719" y="211.993164"/>
<use xlink:href="#glyph-0-4" x="958.136719" y="211.993164"/>
<use xlink:href="#glyph-0-13" x="963.136719" y="211.993164"/>
<use xlink:href="#glyph-0-21" x="968.136719" y="211.993164"/>
<use xlink:href="#glyph-0-18" x="972.136719" y="211.993164"/>
<use xlink:href="#glyph-0-22" x="976.136719" y="211.993164"/>
<use xlink:href="#glyph-0-2" x="979.136719" y="211.993164"/>
<use xlink:href="#glyph-0-13" x="981.136719" y="211.993164"/>
<use xlink:href="#glyph-0-5" x="986.136719" y="211.993164"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-0" x="1000.066406" y="211.993164"/>
<use xlink:href="#glyph-0-1" x="1006.066406" y="211.993164"/>
<use xlink:href="#glyph-0-21" x="1011.066406" y="211.993164"/>
<use xlink:href="#glyph-0-5" x="1015.066406" y="211.993164"/>
<use xlink:href="#glyph-0-14" x="1020.066406" y="211.993164"/>
<use xlink:href="#glyph-0-5" x="1022.066406" y="211.993164"/>
<use xlink:href="#glyph-0-18" x="1027.066406" y="211.993164"/>
<use xlink:href="#glyph-0-15" x="1031.066406" y="211.993164"/>
</g>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(0%, 0%, 0%)" stroke-opacity="1" stroke-miterlimit="10" d="M 46.152344 200.957031 L 46.152344 23.246094 "/>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-34" x="36.21875" y="195.485352"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-35" x="26.21875" y="161.458008"/>
<use xlink:href="#glyph-0-36" x="31.21875" y="161.458008"/>
<use xlink:href="#glyph-0-34" x="36.21875" y="161.458008"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-36" x="26.21875" y="127.430664"/>
<use xlink:href="#glyph-0-34" x="31.21875" y="127.430664"/>
<use xlink:href="#glyph-0-34" x="36.21875" y="127.430664"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-37" x="26.21875" y="93.40332"/>
<use xlink:href="#glyph-0-36" x="31.21875" y="93.40332"/>
<use xlink:href="#glyph-0-34" x="36.21875" y="93.40332"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-38" x="21.21875" y="59.379883"/>
<use xlink:href="#glyph-0-34" x="26.21875" y="59.379883"/>
<use xlink:href="#glyph-0-34" x="31.21875" y="59.379883"/>
<use xlink:href="#glyph-0-34" x="36.21875" y="59.379883"/>
</g>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 43.410156 192.882812 L 46.152344 192.882812 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 43.410156 158.855469 L 46.152344 158.855469 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 43.410156 124.828125 L 46.152344 124.828125 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 43.410156 90.800781 L 46.152344 90.800781 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 43.410156 56.777344 L 46.152344 56.777344 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(0%, 0%, 0%)" stroke-opacity="1" stroke-miterlimit="10" d="M 46.152344 415.847656 L 46.152344 238.136719 "/>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-34" x="36.21875" y="410.37207"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-36" x="31.21875" y="347.266602"/>
<use xlink:href="#glyph-0-34" x="36.21875" y="347.266602"/>
</g>
<g fill="rgb(30.196078%, 30.196078%, 30.196078%)" fill-opacity="1">
<use xlink:href="#glyph-0-38" x="26.21875" y="284.157227"/>
<use xlink:href="#glyph-0-34" x="31.21875" y="284.157227"/>
<use xlink:href="#glyph-0-34" x="36.21875" y="284.157227"/>
</g>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 43.410156 407.769531 L 46.152344 407.769531 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 43.410156 344.664062 L 46.152344 344.664062 "/>
<path fill="none" stroke-width="1.066978" stroke-linecap="butt" stroke-linejoin="round" stroke="rgb(20%, 20%, 20%)" stroke-opacity="1" stroke-miterlimit="10" d="M 43.410156 281.554688 L 46.152344 281.554688 "/>
<g fill="rgb(0%, 0%, 0%)" fill-opacity="1">
<use xlink:href="#glyph-1-0" x="520.835938" y="441.147461"/>
<use xlink:href="#glyph-1-1" x="526.835938" y="441.147461"/>
<use xlink:href="#glyph-1-2" x="532.835938" y="441.147461"/>
<use xlink:href="#glyph-1-2" x="541.835938" y="441.147461"/>
<use xlink:href="#glyph-1-3" x="550.835938" y="441.147461"/>
<use xlink:href="#glyph-1-4" x="556.835938" y="441.147461"/>
<use xlink:href="#glyph-1-5" x="562.835938" y="441.147461"/>
</g>
<g fill="rgb(0%, 0%, 0%)" fill-opacity="1">
<use xlink:href="#glyph-2-0" x="14.108398" y="233.046875"/>
<use xlink:href="#glyph-2-1" x="14.108398" y="227.046875"/>
<use xlink:href="#glyph-2-2" x="14.108398" y="221.046875"/>
<use xlink:href="#glyph-2-3" x="14.108398" y="215.046875"/>
<use xlink:href="#glyph-2-4" x="14.108398" y="209.046875"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 127 KiB

View file

@ -1,417 +0,0 @@
+++
title="Does Aerogramme use too much memory?"
date=2024-02-15
+++
*"Will Aerogramme use too much RAM?" was the first question we asked ourselves
when designing email mailboxes as an encrypted event log, which is very different from
existing designs that are very optimized. This blog post
tries to evaluate our design assumptions to the real world implementation,
similarly to what we have done [on Garage](https://garagehq.deuxfleurs.fr/blog/2022-perf/).*
<!-- more -->
---
## Methodology
Brendan Gregg, a very respected figure in the world of system performances, says that, for many reasons,
[~100% of benchmarks are wrong](https://www.brendangregg.com/Slides/Velocity2015_LinuxPerfTools.pdf).
This benchmark will be wrong too in multiple ways:
1. It will not say anything about Aerogramme performances in real world deployments
2. It will not say anything about Aerogramme performances compared to other email servers
However, I pursue a very specific goal with this benchmark: validating if the assumptions we have done
during the design phase, in term of compute and memory complexity, holds for real.
I will observe only two metrics: the CPU time used by the program (everything except idle and iowait based on the [psutil](https://pypi.org/project/psutil/) code) - for the computing complexity - and the [Resident Set Size](https://en.wikipedia.org/wiki/Resident_set_size) (data held RAM) - for the memory complexity.
<!--My baseline will be the compute and space complexity of the code that I have in mind. For example,
I know we have a "3 layers" data model: an index stored in RAM, a summary of the emails stored in K2V, a database, and the full email stored in S3, an object store.
Commands that can be solved only with the index should use a very low amount of RAM compared to . In turn, commands that require the full email will require to fetch lots of data from S3.-->
## Testing environment
I ran all the tests on my personal computer, a Dell Inspiron 7775 with an AMD Ryzen 7 1700, 16GB of RAM, an encrypted SSD, on NixOS 23.11.
The setup is made of Aerogramme (compiled in release mode) connected to a local, single node, Garage server.
Observations and graphs are done all in once thanks to the [psrecord](https://github.com/astrofrog/psrecord) tool.
I did not try to make the following values reproducible as it is more an exploration than a definitive review.
## Mailbox dataset
I will use [a dataset of 100 emails](https://git.deuxfleurs.fr/Deuxfleurs/aerogramme/src/commit/0b20d726bbc75e0dfd2ba1900ca5ea697645a8f1/tests/emails/aero100.mbox.zstd) I have made specifically for the occasion.
It contains some emails with various attachments, some emails with lots of text, emails generated by many different clients (Thunderbird, Geary, Sogo, Alps, Outlook iOS, GMail iOS, Windows Mail, Postbox, Mailbird, etc.), etc.
The mbox file weighs 23MB uncompressed.
One question that arise is: how representative of a real mailbox is this dataset? While a definitive response is not possible, I compared the email sizes of this dataset to the 2&nbsp;367 emails in my personal inbox.
Below I plotted the empirical distribution for both my dataset and my personal inbox (note that the x axis is not linear but logarithimic).
![ECDF mailbox](ecdf_mbx.svg)
*[Get the 100 emails dataset](https://git.deuxfleurs.fr/Deuxfleurs/aerogramme/src/commit/0b20d726bbc75e0dfd2ba1900ca5ea697645a8f1/tests/emails/aero100.mbox.zstd) - [Get the CSV used to plot this graph](https://git.deuxfleurs.fr/Deuxfleurs/aerogramme/src/branch/perf/cpu-ram-bottleneck/tests/emails/mailbox_email_sizes.csv)*
We see that the curves are close together and follow the same pattern: most emails are between 1kB and 100kB, and then we have a long tail (up to 20MB in my inbox, up to 6MB in the dataset).
It's not that surprising: on many places on the Internet, the limit on emails is set to 25MB. Overall I am quite satisfied by this simple dataset, even if having one or two bigger emails could make it even more representative of my real inbox...
Mailboxes with only 100 emails are not that common (mine has 2k emails...), so to emulate bigger mailboxes, I simply inject the dataset multiple times (eg. 20 times for 2k emails).
## Command dataset
Having a representative mailbox is a thing, but we also need to know what are the typical commands that are sent by IMAP clients.
As I have setup a test instance of Aerogramme (see [my FOSDEM talk](https://fosdem.org/2024/schedule/event/fosdem-2024-2642--servers-aerogramme-a-multi-region-imap-server/)),
I was able to extract 4&nbsp;619 IMAP commands sent by various clients. Many of them are identical, and in the end, only 248 are truly unique.
The following bar plot depicts the command distribution per command name; top is the raw count, bottom is the unique count.
![Commands](command-run.svg)
*[Get the IMAP command log](https://git.deuxfleurs.fr/Deuxfleurs/aerogramme/src/branch/perf/cpu-ram-bottleneck/tests/emails/imap_commands_dataset.log) - [Get the CSV used to plot this graph](https://git.deuxfleurs.fr/Deuxfleurs/aerogramme/src/branch/perf/cpu-ram-bottleneck/tests/emails/imap_commands_summary.csv)*
First, we can handle separately some commands: LOGIN, CAPABILITY, ENABLE, SELECT, EXAMINE, CLOSE, UNSELECT, LOGOUT as they are part of a **connection workflow**.
We do not plan on studying them directly as they will be used in all other tests.
CHECK, NOOP, IDLE, and STATUS are different approaches to detect a change in the current mailbox (or in other mailboxes in the case of STATUS),
I assimilate these commands as a **notification** mechanism.
FETCH, SEARCH and LIST are **query** commands, the first two ones for emails, the last one for mailboxes.
FETCH is from far the most used command (1187 occurencies) with the most variations (128 unique combination of parameters).
SEARCH is also used a lot (658 occurencies, 14 unique).
APPEND, STORE, EXPUNGE, MOVE, COPY, LSUB, SUBSCRIBE, CREATE, DELETE are commands to **write** things: flags, emails or mailboxes.
They are not used a lot but some writes are hidden in other commands (CLOSE, FETCH), and when mails arrive, they are delivered through a different protocol (LMTP) that does not appear here.
In the following, we will assess that APPEND behaves more or less than a LMTP delivery.
<!--
Focus on `FETCH` (128 unique commands), `SEARCH` (14 unique commands)
```
FETCH *:5 (UID ENVELOPE BODY.PEEK[HEADER.FIELDS("References")])
UID FETCH 1:* (UID FLAGS) (CHANGEDSINCE 22)
FETCH 1:1 (UID FLAGS INTERNALDATE RFC822.SIZE BODY.PEEK[HEADER.FIELDS("DATE" "FROM" "SENDER" "SUBJECT" "TO" "CC" "MESSAGE-ID" "REFERENCES" "CONTENT-TYPE" "CONTENT-DESCRIPTION" "IN-REPLY-TO" "REPLY-TO" "LINES" "LIST-POST" "X-LABEL" "CONTENT-CLASS" "IMPORTANCE" "PRIORITY" "X-PRIORITY" "THREAD-TOPIC" "REPLY-TO" "AUTO-SUBMITTED" "BOUNCES-TO" "LIST-ARCHIVE" "LIST-HELP" "LIST-ID" "LIST-OWNER" "LIST-POST" "LIST-SUBSCRIBE" "LIST-UNSUBSCRIBE" "PRECEDENCE" "RESENT-FROM" "RETURN-PATH" "Newsgroups" "Delivery-Date")])
UID FETCH 1:2,11:13,18:19,22:26,33:34,60:62 (FLAGS) (CHANGEDSINCE 165)
UID FETCH 1:7 (UID RFC822.SIZE BODY.PEEK[])
UID FETCH 12:13 (INTERNALDATE UID RFC822.SIZE FLAGS MODSEQ BODY.PEEK[HEADER])
UID FETCH 2 (RFC822.HEADER BODY.PEEK[2]<0.10240>)
```
Flags, date, headers
-->
In the following, I will keep these 3 categories: **writing**, **notification**, and **query** to evaluate Aerogramme's ressource usage
based on command patterns observed on real IMAP commands and the provided dataset.
---
## Write Commands
We start by the write commands as it will enable us to fill the mailboxes for the following evaluations.
I inserted the full dataset (100 emails) to 16 accounts (in other words, in the end, the server handles 1 600 emails) with APPEND.
*[Get the Python script](https://git.deuxfleurs.fr/Deuxfleurs/aerogramme/src/branch/main/tests/instrumentation/mbox-to-imap.py)*
### Filling a mailbox
![Append Custom Build](01-append-tokio-console-musl.png)
First, I observed this *scary* linear memory increase. It seems we are not releasing some memory,
and that's an issue! I quickly suspected tokio-console of being the culprit.
A quick search lead me to an issue entitled [Continuous memory leak with console_subscriber #184](https://github.com/tokio-rs/console/issues/184)
that confirmed my intuition.
Instead of waiting for an hour or trying to tweak the retention time, I built Aerogramme without tokio console.
*So in a first approach, we observed the impact of tokio console instead of our code! Still, we want to
have performances as predictable as possible.*
![Append Cargo Release](02-append-glibc.png)
Which got us to this second pattern: a stable but high memory usage compared to previous run.
It appears I built the binary with `cargo release`, which creates a binary that dynamically link to the GNU libc.
The previous binary was built with our custom Nix toolchain that statically link musl libc to our binary.
In the process, we changed the allocator: it seems the GNU libc allocator allocates bigger chunks at once.
*It would be wrong to conclude the musl libc allocator is more efficient: allocating and unallocating
memory on the kernel side is costly, and thus it might be better for the allocator to keep some kernel allocated memory
for future memory allocations that will not require system calls. This is another example of why this benchmark is wrong: we observe
the memory allocated by the allocator, not the memory used by program itself.*
For the next graph, I removed tokio-console and built Aerogramme with a static musl libc.
![Append Custom Build](03-append-musl.png)
The observed patterns match way better what I was expecting.
We observe 16 spikes of memory allocation, around 50MB, followed by a 25MB memory usage.
In the end, we drop to ~18MB.
In this scenario, we can say that a user needs between 32MB of RAM and 7MB.
In the previous runs, we were doing the inserts sequentially. But in the real world, multiple users interact with the server
at the same time. In the next run, we run the same test but in parrallel.
![Append Parallel](04-append-parallel.png)
We see 2 spikes: a short one at the beggining, and a longer one at the end.
The first spike is probably due to the argon2 decoding, a key derivation function
that is purposedly built to be expensive in term of RAM and CPU.
The second spike is due to the fact that big emails (multiple MB) are at the end of the dataset,
and they are stored fully in RAM before being sent. However, our biggest email weighs 6MB,
and we are running 16 threads, so we should expect around a memory usage that is around 100MB,
not 400MB. This difference would be a good starting point for an investigation: we might
copy a same email multiple times in RAM.
It seems that in this first test that Aerogramme is particularly sensitive to 1) login commands due to argon2 and 2) large emails.
### Re-organizing your mailbox
You might need to organize your folders, copying or moving your email across your mailboxes.
COPY is a standard IMAP command, MOVE is an extension.
I will focus on a brutal test: copying 1k emails from the INBOX to Sent, then moving these 1k emails to Archive.
Below is the graph depicting Aerogramme resource usage during this test.
![Copy and move](copy-move.png)
Memory usage remains stable and low (below 25MB), but the operations are CPU intensive (close to 100% for 40 seconds).
Both COPY and MOVE depict the same pattern: indeed, as emails are considered immutable, Aerogramme only handle pointers in both cases
and do not really copy their content.
Real world clients would probably not send such brutal commands, but would do it progressively, either one by one, or with small batches,
to keep the UI responsive.
While CPU optimizations could probably be imagined, I find this behavior satisfying, especially as memory remains stable and low.
### Messing with flags
Setting flags (Seen, Deleted, Answered, NonJunk, etc.) is done through the STORE command.
Our run will be made in 3 parts: 1) putting one flag on one email, 2) putting 16 flags on one email, and 3) putting one flag on 1k emails.
The result is depicted in the graph below.
![Store flags](store.png)
<!--
`STORE` (19 unique commands).
UID, not uid, silent, not silent, add not set, standard flags mainly.
```
UID STORE 60:62 +FLAGS (\Deleted \Seen)
STORE 2 +FLAGS.SILENT \Answered
```
-->
The first and last spike are due respectively to the LOGIN/SELECT and CLOSE/LOGOUT commands.
We thus have 3 CPU spikes, one for each command, memory remains stable.
The last command is bar far the most expensive, and indeed, it has to generate 1k events in our event log and rebuild many things in the index.
However, there is no reason for the 2nd command to be less expensive than the first one except from the fact it reuses some ressources / cache entries
from the first request.
Interacting with the index is really efficient in term of memory. Generating many changes
lead to high CPU (and possibly lot of IO), but from our dataset we observe most changes are done on one or two emails
and never on all the mailbox.
Interacting with flags should not be an issue for Aerogramme in the near future.
## Notification Commands
Notification commands are expected to be run regularly in background by clients.
They are particularly sensitive as they are correlated to your number of users,
independently of the number of emails they receive. I split them in 2 parts:
the ones that are intermittent, and like HTTP, closes the connection after being run,
and the ones that are continuous, where the socket is maintained open forever.
### The cost of a refresh
NOOP, CHECK, STATUS are commands that trigger a refresh of the IMAP
view, and are part of the "intermittent" commands. In some ways, the SELECT and/or EXAMINE
commands could also be interpreted as a notification command: a client that is configured
to poll a mailbox every 15 minutes will not use the NOOP, running EXAMIME will be enough.
In our case, all these commands are similar in the sense that they load or refresh the in-memory index
of the targeted mailbox. To illustrate my point, I will run SELECT, NOOP, CHECK, and STATUS on another mailbox in a row.
![Refresh plot](refresh.png)
The first CPU spike is LOGIN/SELECT, the second is NOOP, the third CHECK, the last one STATUS.
CPU spikes are short, memory usage is stable.
Refresh commands should not be an issue for Aerogramme in the near future.
### Continuously connected clients
IDLE (and NOTIFY that is currently not implemented in Aerogramme) are commands
that maintain a socket opened. These commands are sensitive, as while many protocols
are one shot, and then your users spread their requests over time, with these commands,
all your users are continuously connected.
In the graph below, we plot the resource usage of 16 users with a 100 emails mailbox each that log into the system,
select their inbox, switch to IDLE, and then, one by one, they receive an email and are notified.
![Idle Parallel](05-idle-parallel.png)
Memory usage is linear with the number of users.
If we extrapolate this observation, it would imply that 1k users = 2GB of RAM.
That's not something negligible, and it should be observed closely.
In the future, if it appears that's an issue, we could consider optimizations like 1) unloading the mailbox index
and 2) mutualizing the notification/wake up mechanism.
## Query Commands
Query commands are the most used commands in IMAP,
they are very expressive and allows the client to fetch only what they need:
a list of the emails without their content, displaying an email body without
having to fetch its attachment, etc.
Of course, this expressivity creates complexity!
### Fetching emails
Often, IMAP clients in first instance, are only interested by email metadata.
For example, the ALL keyword fetches some metadata, like flags, size, sender, recipient, etc.
Ressource usage of fetching this information on 1k email is depicted below.
![Fetch All 1k mail](06-fetch-all.png)
CPU spike is short, memory usage is low: nothing alarming in term of performances.
IMAP standardizes another keyword, FULL, that also returns the "shape" of a MIME email as an S-Expression.
Indeed, MIME emails can be seen as a tree where each node/leaves are a "part".
In Aerogramme, this shape is - as of 2024-02-17 - not pre-computed and not save in database, and thus, the full email must be fetched and parsed.
So, when I tried to fetch this shape on 1k emails, Garage crashed:
```
ERROR hyper::server::tcp: accept error: No file descriptors available (os error 24)
```
Indeed, `ulimit` is set to 1024 on my machine, and apparently, I tried to open more than 1024 descriptors
for a single request... It's definitely an issue that must be fixed, but for this article,
I will increase the limit to make the request succeed.
I get the following graph.
![Fetch Full 1k mail](07-fetch-full.png)
With a spike at 300MB, it's clear we are fetching the full mailbox before starting to process it.
While it's a performance issue, it's also a stability/predictability issue: any user could trigger huge allocations on the server.
### Searching
<!--
```
3 SEARCH HEADER "Message-ID" "<4a83801e-4848-fbe5-0afa-ef8592d99a52@saint-ex.deuxfleurs.org>" UNDELETED SINCE 1-Jan-2020
```
-->
<!--
```
SEARCH UNDELETED SINCE 2023-11-17
UID SEARCH HEADER "Message-ID" "<x@y.z>" UNDELETED
UID SEARCH 1:* UNSEEN
UID SEARCH BEFORE 2024-02-09
```
-->
First, we start with a SEARCH command inspired by what we have seen in the logs on the whole mailbox, and that can be run
without fetching the full email from the blob storage.
![Search meta](search-meta.png)
Spike order: 1) artifact, ignored, 2) login+select, 3) search, 4) logout
We load ~10MB in memory to make our request that is quite fast.
But we also know that some SEARCH requests will require to fetch some content
from the S3 object storage, and in this case, the profile is different.
![Search body](search-body.png)
We have the same profile as FETCH FULL: a huge allocation of memory and a very CPU intensive task.
The conclusion is similar to FETCH: while these commands are OK to be slow, it's not OK to allocate so much memory.
### Listing mailboxes
Another object that can be queried in IMAP are mailboxes, through the LIST command.
The test consists of 1) LOGIN, 2) LIST, and 3) LOGOUT done on a user account with 5 mailboxes.
![List mailboxes](list.png)
There are only 2 spikes (LOGIN and LOGOUT), as the mailbox list is loaded eagerly when the user connects.
Because it's a small datastructure, it's quick to parse it, which explains why there is no CPU/memory spike
for the LIST command in itself.
---
## Discussion
At this level of maturity, the main goal for Aerogramme is predictable & stable resource usage server side.
Indeed, there is nothing more annoying than a user, honest or malicious, breaking the server while running
a resource intensive command.
**Querying bodies** - They may allocate the full mailbox in RAM. For some parameters (eg. FETCH BODY),
it might be possible to precompute data, but for some others (eg. SEARCH TEXT "something") it's not possible, so precomputing
is not a definitive solution. Also being slow is acceptable here: we just want to avoid being resource intensive. The solution I envision is to "stream"
the fetched emails and process them one by one. For some commands like FETCH 1:* BODY[], that are to the best of my knowledge,
never run by real clients, it will not be enough however. Indeed, Aerogramme will drop the fetched emails, but it will have an in-memory copy inside
the response object awaiting for delivery. So we might want to implement response streaming too.
**argon2** - Login resource usage is due to argon2, but compared to many other protocols, authentications in IMAP occure way more often.
For example, if a user configure their email client to poll their mailbox every 5 minutes, this client will authenticate 288 times in a single day!
argon2 resource usage is a tradeoff with the bruteforcing difficulty, reducing its resource usage is thus a possible performance mitigation
solution at the cost of reduced security. Other options might reside in the evolution of our authentication system: even if I don't
know what is the current state of implementation of OAUTH in existing IMAP clients, it could be considered as an option.
Indeed, the user enters their password once, during the configuration of their account, and then a token is generated and stored in the client.
The credentials could be stored in the token (think a JSON Web Token for example), avoiding the expensive KDF on each connection.
**IDLE** - IDLE RAM usage is the same as other commands, but IDLE keeps the RAM allocated for way longer.
To illustrate my point, let's suppose an IMAP session uses 5MB of RAM,
and two populations of 1 000 users. We suppose users monitor only one mailbox (which is not true in many cases).
The first population, aka *the poll population*, configures their client to poll the server every 5 minutes, polling takes 2 seconds (LOGIN + SELECT + LOGOUT).
The second population, aka *the push population*, configures their client to "receive push messages" - ie. using IMAP IDLE.
For the *push population*, the RAM usage will
be a stable 5GB (5MB * 1 000 users): all users will be always connected.
With a 1MB session, we could reduce the RAM usage to 1GB: any improvement on the session base RAM
will be critical to IDLE with the current design.
For the *poll population*, we can split the time in 150 ticks (5 minutes / 2 seconds = 150 ticks).
It seems the problem can be mapped to a [Balls into bins problem](https://en.wikipedia.org/wiki/Balls_into_bins_problem)
with random allocation (yes, I know, assuming random allocation might not hold in many situations).
Based on the Wikipedia formula, if I am not wrong, we can suppose that, with high probability, at most 13 clients will be connected
at once, which means 65MB of RAM usage (5MB * 13 clients/tick = 65MB/tick).
With these *back-of-the-envelope calculations*, we understand how crucial the IDLE RAM consumption is compared
to other commands, and how the base RAM consumption of a user will impact the service.
**Large email streaming** - Finally, email streaming could really improve RAM usage for large emails, especially on APPEND and LMTP delivery,
or even on FETCH when the body is required. But implementing such a feature would require an email parser that
can work on streams, which in turns [is not something trivial](https://github.com/rust-bakery/nom/issues/1160).
While it seems untimely to act now, these spots are great candidates for a closer monitoring for future performance evaluations.
Fixing these points, above the simple mitigations, will involve important design changes in Aerogramme, which means
in the end: writing lot of code! That's why I think Aerogramme can work with these "limitations" for now,
and we will take decisions about these points when it will be *really* required.
## Conclusion
Back to the question "Does Aerogramme use too much RAM?",
it of course depends on the context.
We want to start with 1 000 users, a 1k email INBOX, and a server of 8GB of RAM,
and Aerogramme seems ready for a first deployment in this context.
So for now, the answer could be "No, it does not".
Of course, in the long term,
we expect better ressource usages, and I am convinced that for many people, today the answer is still "Yes, it does".
Based on this benchmark, I identified 3 low-hanging fruits to improve performances that do not require major design changes: 1) in FETCH+SEARCH queries, handling emails one after another instead of loading the full mailbox in memory, 2) streaming FETCH responses instead of aggregating them in memory, and 3) reducing the RAM usage of a base user by tweaking its Garage connectors configuration.
Collecting production data will then help priorize other, more ambitious works, on the authentication side,
on the idling side, and on the email streaming side.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

View file

@ -1,8 +1,6 @@
+++
title = "Blog"
description = "This is our developer journal"
template = "blog_index.html"
page_template = "blog_article.html"
sort_by = "date"
paginate_by = 5
+++
+++

View file

@ -1,6 +0,0 @@
+++
template = "documentation.html"
page_template = "documentation.html"
redirect_to = "documentation/quick-start/"
+++

Binary file not shown.

Before

Width:  |  Height:  |  Size: 550 KiB

View file

@ -1,6 +0,0 @@
+++
title = "CalDAV"
weight = 10
+++
*not yet implemented*

View file

@ -1,6 +0,0 @@
+++
title = "CardDAV"
weight = 20
+++
*not yet implemented*

View file

@ -1,6 +0,0 @@
+++
title = "Proxy"
weight = 40
+++
*Not yet written*

View file

@ -1,6 +0,0 @@
+++
title = "WASM"
weight = 50
+++
*Not yet implemented*

View file

@ -1,24 +0,0 @@
+++
title = "Connect"
weight = 20
sort_by = "weight"
template = "documentation.html"
+++
This section references all the clients that can be connected to Aerogramme,
what features they can use, how well they behave.
## Standard protocols
- [IMAP](@/documentation/connect/imap.md) - Access to your mailbox
<!-- - [CalDAV](@/documentation/connect/caldav.md) - Access to your calendars
- [CardDAV](@/documentation/connect/carddav.md) - Access to your contacts
-->
## Client-side encryption
- **Companion Proxy** - Companion proxy is not yet documented as it will be refined in the near future.
<!--
- [Proxy](@/documentation/connect/proxy.md) - Proxy approach to access to your profile
- [WASM](@/documentation/connect/wasm.md) - Library approach to access to your profile
-->

View file

@ -1,23 +0,0 @@
+++
title = "IMAP"
weight = 5
+++
The following clients are known to work with Aerogramme:
| Client | Date | Aerogramme version | Status |
|----------------|------------|--------------------|--------|
| Mutt | 2023 | pre 0.1 | ✅ |
| Thunderbird | 2024-01-08 | pre 0.2 | ✅ |
| Geary | 2024-01-08 | pre 0.2 | ✅ |
| Evolution | 2024-01-08 | pre 0.2 | ✅ |
| K9 Mail | 2024-01-08 | pre 0.2 | ✅ |
| Fair Email | 2024-01-08 | pre 0.2 | ✅ |
| Apple Mail iOS | 2024-01-08 | pre 0.2 | ✅ |
| Outlook iOS | 2024-01-08 | pre 0.2 | ✅ |
| Gmail Android | 2024-01-08 | pre 0.2 | ✅ |
| Windows Mail | 2024-01-08 | pre 0.2 | ✅ |
If you find a regression, please report it.
If you use a well-known email client, that work or not work with Aerogramme,
please report it here, so we can track Aerogramme's IMAP compatibility with the rest of the ecosystem.

View file

@ -1,6 +0,0 @@
+++
title = "Auto-discovery"
weight = 90
+++
Todo

View file

@ -1,6 +0,0 @@
+++
title = "Backups"
weight = 120
+++
Todo

View file

@ -1,6 +0,0 @@
+++
title = "Distributed Storage"
weight = 70
+++
Todo

View file

@ -1,6 +0,0 @@
+++
title = "LDAP User Management"
weight = 60
+++
LDAP

View file

@ -1,6 +0,0 @@
+++
title = "Hardened configuration"
weight = 130
+++
Todo

View file

@ -1,6 +0,0 @@
+++
title = "Observability"
weight = 110
+++
Todo

View file

@ -1,6 +0,0 @@
+++
title = "Orchestrators"
weight = 80
+++
Todo

View file

@ -1,6 +0,0 @@
+++
title = "Updates"
weight = 100
+++
Todo

View file

@ -1,50 +0,0 @@
+++
title = "Cookbook"
weight = 10
sort_by = "weight"
template = "documentation.html"
+++
This cookbook will guide you from your first steps
to a real deployment of Aerogramme.
## Single-node, minimal deployment
A minimal deployment will allow you to deploy Aerogramme on a single-server
in a way that it works for a small production deployment.
However, as Aerogramme is a distributed-first server,
you will not benefit from all of its nice properties, and many manual
work will be required.
- [Local storage](@/documentation/cookbook/single-node-garage-storage.md)
- [Configuration file](@/documentation/cookbook/config.md)
- [User management](@/documentation/cookbook/static-user-management.md)
- [TLS encryption](@/documentation/cookbook/tls-encryption.md)
- [Integration with a service manager](@/documentation/cookbook/service-manager.md) (systemd or docker)
- [SMTP server integration](@/documentation/cookbook/smtp-server.md) (MTA)
*Multi-node deployments and lifecycle maintainance are not covered yet.*
<!--
## Multi-nodes, standard deployment
Aerogramme is intended for multi-nodes deployment. This guide
will cover the specificities introduced by a multi-node deployment.
- [LDAP user management](@/documentation/cookbook/ldap.md)
- [Distributed storage](@/documentation/cookbook/garage-cluster-storage.md)
- [Orchestrators](@/documentation/cookbook/orchestrators.md)
- [Auto-discovery](@/documentation/cookbook/autodiscovery.md)
## Lifecycle
Hopefully you will love Aerogramme, and thus you will have to do some maintainance on it.
- [Updates](@/documentation/cookbook/updates.md)
- [Observability](@/documentation/cookbook/observability.md)
- [Backups](@/documentation/cookbook/backups.md)
## Hardened flavor
- [Manual configuration](@/documentation/cookbook/manual-hardened.md)
-->

View file

@ -1,37 +0,0 @@
+++
title = "Configuration file"
weight = 10
+++
In the [quickstart](@/documentation/quick-start/_index.md), you launched Aerogramme in "development mode", that do not require a configuration file. But for a real-world usage, you will need to specificy many things: how your users are managed, which port to use, how and where data is stored, etc.
This page describes how to write your first configuration file.
If you want a complete reference, check the dedicated [Configuration Reference](@/documentation/reference/config.md) page.
```toml
role = "Provider"
pid = "aerogramme.pid"
[imap_unsecure]
bind_addr = "[::]:1143"
[auth]
bind_addr = "[::1]:12345"
[lmtp]
bind_addr = "[::1]:1025"
hostname = "example.tld"
[users]
user_driver = "Static"
user_list = "users.toml"
```
Copy this content in a file named `aerogramme.toml`.
Also create an empty file named `users.toml` (Aerogramme does not know how to create it automatically yet).
And then you can start Aerogramme with the following configuration file:
```bash
aerogramme -c aerogramme.toml provider daemon
```

View file

@ -1,97 +0,0 @@
+++
title = "Service Managers (eg. systemd)"
weight = 40
+++
You may want to start Aerogramme automatically on boot,
restart it if it crashes, etc. Such actions can be achieved through a service manager.
## systemd
We make some assumptions for this systemd deployment.
- Your garage binary is located at `/usr/local/bin/aerogramme`.
- Your configuration file is located at `/etc/aerogramme/config.toml`.
- If you use Aerogramme's user management, the user list is set to `/etc/aerogramme/users.toml`.
Create a file named `/etc/systemd/system/aerogramme.service`:
```ini
[Unit]
Description=Aerogramme Email Server
After=network-online.target
Wants=network-online.target
[Service]
Environment='RUST_LOG=aerogramme=info' 'RUST_BACKTRACE=1'
ExecStart=/usr/local/bin/aerogramme -c /etc/aerogramme/config.toml provider daemon
DynamicUser=true
ProtectHome=true
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target
```
**A note on hardening:** The Aerogramme daemon is not expected to write on the filesystem.
When you use the `aerogramme provider account`, the write is done by your current user/process,
not the daemon process. That's why we don't define a `StateDirectory`.
To start the service then automatically enable it at boot:
```bash
sudo systemctl start aerogramme
sudo systemctl enable aerogramme
```
To see if the service is running and to browse its logs:
```bash
sudo systemctl status aerogramme
sudo journalctl -u aerogramme
```
To add a new user:
```bash
sudo aerogramme \
-c /etc/aerogramme/config.toml \
provider account add --login alice --setup #...
sudo systemctl reload aerogramme
```
## Other service managers
Other service managers exists: SMF (illumos / solaris), OpenRC (alpine & co), rc (FreeBSD, OpenBSD, NetBSD).
Feel free to open a PR to add some documentation.
You would not use System V initialization scripts...
<!--
## docker-compose
An example docker compose deployment with Garage included:
```yml
version: "3"
services:
aerogramme:
image: registry.deuxfleurs.org/aerogramme:0.2.0
restart: unless-stopped
ports:
- "1025:1025"
- "143:1143"
volumes:
- aerogramme.toml:/aerogramme.toml
- users.toml:/users.toml
garage:
image: docker.io/dxflrs/garage:v0.9.1
restart: unless-stopped
volumes:
- garage.toml:/etc/garage.toml
- garage-meta:/var/lib/garage/meta
- garage-data:/var/lib/garage/data
```
-->

View file

@ -1,52 +0,0 @@
+++
title = "Local storage"
weight = 5
+++
Currently, Aerogramme does not support local storage (ie. storing emails
on your filesystem directly). It might support this feature in the future,
but no ETA is announced yet. In the mean time, we will deploy a single-node
Garage cluster to store the data.
Start by following the [Garage Quickstart](https://garagehq.deuxfleurs.fr/documentation/quick-start/) up to "Creating a cluster layout" (included).
## Setup your storage
Create one key per user:
```bash
garage key create aerogramme
# ...
```
*Do not forget to note the key, it will be needed later.*
Create an Aerogramme bucket for each user:
```bash
garage bucket create aerogramme-alice
garage bucket create aerogramme-bob
garage bucket create aerogramme-charlie
# ...
```
*Having one bucket per user is an opinionated design choice made to support Aerogramme [Per-user storage, per-user encryption](@/documentation/design/per-user-encryption.md) goal.*
And then allow the aerogramme key on each bucket:
```bash
garage bucket allow --read --write --key aerogramme aerogramme-alice
garage bucket allow --read --write --key aerogramme aerogramme-bob
garage bucket allow --read --write --key aerogramme aerogramme-charlie
# ...
```
## Automating it
You can automate this by using the [Garage Admin API](https://garagehq.deuxfleurs.fr/documentation/reference-manual/admin-api/) and the [S3 API](https://garagehq.deuxfleurs.fr/documentation/reference-manual/s3-compatibility/).
## Quota
You can use Garage per-bucket quota to limit the amount of space a user can
use, but such data is not reported on the IMAP side.

View file

@ -1,143 +0,0 @@
+++
title = "SMTP servers"
weight = 50
+++
SMTP servers that are recommended for Aerogramme are the ones that support:
- TCP delivery over the LMTP protocol
- TCP authentication over the [Dovecot SASL Auth protocol](https://doc.dovecot.org/developer_manual/design/auth_protocol/)
Postfix supports these 2 features and is the only recommended choice *for now*.
## Postfix
Configuring [Postfix](https://www.postfix.org/) requires to add these 4 lines to `main.cf`:
```ini
smtpd_sasl_type = dovecot
smtpd_sasl_path = inet:localhost:12345
virtual_mailbox_domains = your-domain.tld
virtual_transport = lmtp:[::1]:1025
```
Aerogramme implements Dovecot SASL protocol. By configuring Postfix
with it,
Make sure that `your-domain.tld` is not already configured in the `mydomain` variable,
or it might conflict with Postfix local delivery logic.
*Indeed, Postfix internally has its default configuration for "local" mail delivery,
that maps to the old way of managing emails. LMTP delivery is a more recent, and maps
to the "virtual" mail delivery mechanisms of Postfix. Your goal is thus to deactivate
as much as possible the "local" delivery capabilities of Postfix and only allow
the "virtual" ones.*
You can learn more about Postfix LMTP capabilities on this page: [lmtp(8)](https://www.postfix.org/lmtp.8.html).
## Maddy
[Maddy](https://maddy.email/) is a more recent email server written in Go.
However it does not support LMTP delivery over TCP, only over UNIX socket: without a specific adapter, it's not yet compatible with Aerogramme.
For LMTP delivery, read [SMTP & LMTP transparent forwarding](https://maddy.email/reference/targets/smtp/#smtp-lmtp-transparent-forwarding).
For the Dovecot Auth Protocol, read [Dovecot SASL](https://maddy.email/reference/auth/dovecot_sasl/).
## OpenSMTPD
Something like below might work (untested):
```bash
action "remote_mail" lmtp "/var/run/dovecot/lmtp" rcpt-to virtual <virtuals>
match from any for domain "your-domain.tld" action "remote_mail"
```
The syntax is described in their manpage [smtpd.conf(5)](https://man.openbsd.org/smtpd.conf#lmtp).
opensmtpd does not support Dovecot's SASL protocol, you can signal your interest [in their dedicated issue](https://github.com/OpenSMTPD/OpenSMTPD/issues/1085).
## Chasquid
[chasquid](https://blitiri.com.ar/p/chasquid/) supports [LMTP delivery](https://blitiri.com.ar/p/chasquid/howto/#configure-chasquid)
and the [Dovecot Auth Protocol](https://blitiri.com.ar/p/chasquid/docs/dovecot/) but only over UNIX sockets. Thus, it's not yet compatible with Aerogramme.
## Other servers
[Exim](https://www.exim.org/) has some support [for LMTP](https://www.exim.org/exim-html-current/doc/html/spec_html/ch-the_lmtp_transport.html) too.
[sendmail](https://www.proofpoint.com/us/products/email-protection/open-source-email-solution) might deliver to LMTP through a dedicated binary named [smtpc](https://www.sympa.community/manual/customize/lmtp-delivery.html).
<!--
Let start by creating a folder for Postfix, for example `/opt/aerogramme-postfix`:
```bash
mkdir /tmp/aerogramme-postfix
cd /opt/aerogramme-postfix
mkdir queue
```
To run Postfix, you need some users / groups setup (do it in a container if you don't want to mess up your system):
```bash
sudo useradd postfix
sudo groupadd postdrop
```
The considered `main.cf`:
```
# postfix files
queue_directory=/tmp/postfix-test/queue
data_directory=/tmp/postfix-test/data
maillog_file=/dev/stdout
# nuke postfix legacy as much as possible (an era of UNIX account and open relay on local networks...)
mynetworks=127.0.0.0/8
compatibility_level=3.6
alias_database=
alias_maps=
# add support for authentication
smtpd_sasl_auth_enable=yes
smtpd_tls_auth_only = yes
smtpd_relay_restrictions =
permit_sasl_authenticated
reject_unauth_destination
# add support for TLS (RSA only for now)
smtpd_tls_cert_file=/home/quentin/.lego/certificates/saint-ex.deuxfleurs.org.crt
smtpd_tls_key_file=/home/quentin/.lego/certificates/saint-ex.deuxfleurs.org.key
# aerogramme specific configuration
smtpd_sasl_type = dovecot
smtpd_sasl_path = inet:localhost:12345
virtual_mailbox_domains=saint-ex.deuxfleurs.org
virtual_transport=lmtp:[::1]:1025
```
The considered `master.cf`:
```
smtp inet n - n - - smtpd
smtp unix - - n - - smtp
smtps inet n - n - - smtpd
-o smtpd_tls_wrappermode=yes
-o smtpd_sasl_auth_enable=yes
-o smtpd_client_restrictions=permit_sasl_authenticated,reject
-o milter_macro_daemon_name=ORIGINATING
lmtp unix - - n - - lmtp
anvil unix - - n - 1 anvil
rewrite unix - - n - - trivial-rewrite
cleanup unix n - n - 0 cleanup
qmgr fifo n - n 300 1 qmgr
tlsmgr unix - - n 1000? 1 tlsmgr
bounce unix - - n - 0 bounce
defer unix - - n - 0 bounce
trace unix - - n - 0 bounce
error unix - - n - - error
retry unix - - n - - error
discard unix - - n - - discard
virtual unix - n n - - virtual
proxymap unix - - n - - proxymap
postlog unix-dgram n - n - 1 postlogd
```
-->

View file

@ -1,70 +0,0 @@
+++
title = "User Management"
weight = 20
+++
Aerogramme can externalize its user management through [LDAP](@/documentation/reference/config.md), but if you target a simple deployment, it has also an internal user management system that will be covered here.
As a pre-requisite, you must have your `aerogramme.toml` file configured for "Static" user management as described in [Configuration file](@/documentation/cookbook/config.md). You also need a configured Garage instance, either [local](@/documentation/cookbook/single-node-garage-storage.md) or distributed.
## Adding users
Once you have done all the previous pre-requisites, Aerogramme provides a command-line utility to add a user:
```bash
aerogramme provider account add --login alice --setup <(cat <<EOF
email_addresses = [ "alice@example.tld", "alice.smith@example.tld" ]
clear_password = "hunter2"
storage_driver = "Garage"
s3_endpoint = "http://localhost:3900"
k2v_endpoint = "http://localhost:3904"
aws_region = "garage"
aws_access_key_id = "GKa8..."
aws_secret_access_key = "7ba95..."
bucket = "aerogramme-alice"
EOF
)
aerogramme provider account add --login bob --setup # ...
# ...
aerogramme provider account add --login charlie --setup # ...
# ...
```
*You must run this command for all your users.*
If you don't set the `clear_password` field, it will be interactively asked.
This command will edit your `user_list` file. If your Aerogramme daemon is already running, you must reload it in order to load the newly added users. To reload Aerogramme, run:
```bash
aerogramme provider reload
```
## Change account password
You might need to change an account password, you can run:
```bash
aerogramme provider account change-password --login alice
```
You can pass the old and new password through environment variables:
```bash
AEROGRAMME_OLD_PASSWORD=x \
AEROGRAMME_NEW_PASSWORD=y \
aerogramme provider account change-password --login alice
```
*Do not forget to reload*
## Delete account
```bash
aerogramme provider account delete --login alice
```
*Do not forget to reload*

View file

@ -1,64 +0,0 @@
+++
title = "TLS"
weight = 30
+++
In the [Configuration File](@/documentation/cookbook/config.md) page of the cookbook, we configure a cleartext IMAP service
that is unsecure, as anyone spying on the network can intercept the user's password.
## Activate IMAP TLS
You must replace the `[imap_unsecure]` block of your configuration file with a new `[imap]` block:
```toml
[imap]
bind_addr = "[::]:993"
certs = "cert.pem"
key = "key.pem"
```
## Generate self-signed certificates
If you want to quickly try the TLS endpoint, you can generate a self-signed certificate with openssl:
```bash
openssl ecparam -out key.pem -name secp256r1 -genkey
openssl req -new -key key.pem -x509 -nodes -days 365 -out cert.pem
```
This configuration is not secure as it is vulnerable to man-in-the-middle attacks.
It will also triggers a big red warning in many email clients, and sometimes it will even be impossible to configure an account.
## Generate valid certificates through Let's Encrypt
Automated certificate renewal has been popularized by Let's Encrypt through the [ACME protocol](https://en.wikipedia.org/wiki/Automatic_Certificate_Management_Environment).
Today, many certificate providers implement it, like ZeroSSL, Buypass Go SSL, or even Google Cloud.
Many clients that implement the ACME protocol exist (certbot, lego, etc.), [a very long list exist on LE website](https://letsencrypt.org/docs/client-options/).
Finally, certificates can be obtained in exchange of a validation, that can occur over HTTP (HTTP01 challenge) or DNS (DNS01 challenge).
This example will be given for Let's Encrypt with Lego for a DNS01 challenge with Gandi as the DNS provider.
```bash
GANDIV5_API_KEY=xxx \
GANDIV5_PERSONAL_ACCESS_TOKEN=xxx \
lego \
--email you@example.tld \
--dns gandiv5 \
--domain example.tld \
--domains imap.example.tld \
--domains smtp.example.tld \
run
```
*Note 1: theoretically only `GANDIV5_PERSONAL_ACCESS_TOKEN` should be required, but it did not work for me.*
*Note 2: we generate a certificate for the root domain and SMTP because it will simplify your testing while following the cookbook.
But if you already have a working email stack, it's not required.*
If the command ran successfully, you now have 2 files:
- `.lego/certificates/example.tld.crt`
- `.lego/certificates/example.tld.key`
You can directly use them in Aerogramme (the first one must be put on `certs` and the second one on `key`).
You must configure some way to automatically renew your certificates, the [lego documentation](https://go-acme.github.io/lego/usage/cli/renew-a-certificate/) explains how you can do it.

View file

@ -1,33 +0,0 @@
+++
title = "Concepts"
weight = 40
sort_by = "weight"
template = "documentation.html"
+++
## Goals
**Highly resilient** - Multiple instances of Aerogramme can been run in parallel without coordination.
Multi-region support, survive datacenter failures.
**Easy to operate** - Transparently replicate mailbox and solve conflicts. Integrate with your LDAP server. Privacy friendly
**Per-user encryption of mailboxes.**
Can be run as a local proxy to hide your mailbox content from the server.
## Main concepts
[Per-user encryption](@/documentation/design/per-user-encryption.md) - Aerogramme can't persist data in plain text,
instead its whole data model is built upon the idea that a mailbox is a series of encrypted blob. These blobs do not reveal
the mailbox name, the metadata of stored emails or even the flags that have been put on them.
**Continuous Mailbox Merging** - As multiple instances of Aerogramme can be run simultaneously, and that's possible
that 2 instances interact with the same mailbox (over Garage), each process monitors external writes for the mailbox
they track and automatically do the merging [in a correct way](@/documentation/internals/imap_uid.md).
**Modular design** - Login and Mailbox storage is abstracted behind an interface: multiple
implementations are thus possible.
**Microservice** - Aerogramme is stateless and tries to adhere as much as possible to the [12 factor app](https://12factor.net/) principles so it's easy to run in a cluster.

View file

@ -1,47 +0,0 @@
+++
title = "Per-user encryption"
weight = 10
+++
Aerogramme can't store plaintext data, instead all users data must be encrypted with a per-user key on the storage target.
Of course, cryptography is always a tradeoff with other properties (usability, compatibility, features, etc.),
so the way the key is derived and where the encryption/decryption can take place can be configured.
## Compared to PGP
PGP only encrypts the body of the email, it keeps in cleartext the metadata of your email (fields like From:, To:, or Subject: are readable by an attacker),
it can't protect your flags, your mailbox names, etc. Conversely, all this data is encrypted in Aerogramme.
## Security flavors
These different configurations are identified as flavors:
**Transparent Flavor** - Users' data are decrypted in the server's RAM. The key is derived from the user's password upon IMAP login.
It's completely transparent to end users. While an attacker having full access to the server can still compromise users' data, it reduces the attack surface.
In term of security, it's similar to [TREES](https://0xacab.org/liberate/trees) (from [RiseUp](https://riseup.net)) or [scrambler](https://github.com/posteo/scrambler-plugin) (from [Posteo](https://posteo.de/)).
**Hardened Flavor** - Users' data are decrypted on the user's device. The private key is only stored on the user's device.
The server knows only users' public keys. When an email is received by the server, it is directly encrypted and stored by the server.
The user must run Aerogramme locally as a proxy that will manipulate the encrypted blobs stored in the server
and decrypt them locally, exposing an IMAP proxy interface. An attacker having full access to the server at a point in time
will not be able to compromise your already received data (but can intercept new emails). It's similar to [Proton Mail Bridge](https://proton.me/fr/mail/bridge),
but keep in mind that Aerogramme does not support (yet) end-to-end email encryption like Proton Mail or Tutanota, *so Aerogramme is less secure*.
<!--When run on server (both for the transparent and hardened flavor), Aerogramme must be started in the "provider mode", as in "email service provider".
When run on the end-user device (only the hardened flavor require that), Aerogramme must be started in the "companion mode", as in "a companion process of your email client".
These 2 words are materialized as 2 subcommands on the Aerogramme binary: `aerogramme provider` and `aerogramme companion`.-->
## Aerogramme roles
The transparent flavor only requires Aerogramme to be run on the service provider server, while the hardened flavor require the end-user to run a local proxy.
More specifically:
**Provider** - Provider must be run by the service provider, it is used for both flavors. For the transparent flavor, it both receives emails through LMTP and expose
the mailbox through IMAP. For the hardened mode, it only receives emails through LMTP, encrypt them with user's public key, but can't expose them through IMAP as the server
can't decrypt them. Provider commands are available through the `aerogramme provider` subcommand.
**Companion** - Companion must be run by the end user, it is used only for the hardened flavor. It fetches encrypted blobs from the server
of the email provider, decrypt them locally, and expose the mailbox across the IMAP interface, acting as a local proxy.
Companion commands are avaialble through the `aerogramme companion` subcommand.

View file

@ -1,10 +0,0 @@
+++
title = "Development"
weight = 60
sort_by = "weight"
template = "documentation.html"
+++
To help you in the development, you might need:
- [Help debugging the protocol with socat](@/documentation/development/netcat.md)
- [Help finding datasets of email](@/documentation/development/dataset.md)

View file

@ -1,20 +0,0 @@
+++
title = "Datasets"
weight = 20
+++
To debug / fuzz Aerogramme, we seek some datasets.
## Emails datasets
- [stalwartlabs/mail-parser](https://github.com/stalwartlabs/mail-parser/tree/main/tests)
- [basecamp/mail](https://github.com/basecamp/mail/tree/master/spec/fixtures)
- [Enron dataset - 500k entries](https://www.cs.cmu.edu/~enron/)
- [Jeb Bush dataset - 290k entries](https://ab21www.s3.amazonaws.com/JebBushEmails-Text.7z)
- [spambase dataset](https://archive.ics.uci.edu/ml/datasets/spambase) (also contains legit emails)
- mailing lists
- [W3C](https://lists.w3.org/Archives/Public/)
- [Wikimedia](https://lists.wikimedia.org/hyperkitty/)
- [Apache](https://commons.apache.org/mail-lists.html) - [tomcat](https://lists.apache.org/list.html?dev@tomcat.apache.org), [kafka](https://lists.apache.org/list.html?dev@kafka.apache.org).
- [Linux](https://marc.info/?l=linux-kernel)
- your own inbox

View file

@ -1,120 +0,0 @@
+++
title = "Debug with socat"
weight = 10
+++
In this document, we collected some traces from netcat
that could help you quickly test/debug Aerogramme.
Start with:
```
socat - tcp:localhost:1143,crlf
```
## Login
```
S: * OK Hello
C: A1 LOGIN alice hunter2
S: A1 OK Completed
```
## Select mailbox
```
C: A2 SELECT INBOX
S: * 0 EXISTS
S: * 0 RECENT
S: * FLAGS (\Seen \Answered \Flagged \Deleted \Draft)
S: * OK [PERMANENTFLAGS (\Seen \Answered \Flagged \Deleted \Draft \*)] Flags permitted
S: * OK [UIDVALIDITY 1] UIDs valid
S: * OK [UIDNEXT 1] Predict next UID
S: A2 OK [READ-WRITE] Select completed
```
## Check for new mails
Here we simply use the `NOOP` command to trigger an interaction with the server.
```
C: A4 NOOP
S: * 1 EXISTS
S: A4 OK NOOP completed.
```
## See mail structure
```
C: A5 FETCH 1 FULL
S: * 1 FETCH (UID 1 FLAGS () INTERNALDATE "06-Jul-2022 14:46:42 +0000"
RFC822.SIZE 117 ENVELOPE (NIL "test" (("Alan Smith" NIL "alan" "smith.me"))
NIL NIL (("Alan Smith" NIL "alan" "aerogramme.tld")) NIL NIL NIL NIL)
BODY ("TEXT" "test" NIL "test" "test" "test" 1 1))
```
## See mail content
```
C: A6 FETCH 1 (RFC822)
S: * 1 FETCH (UID 1 RFC822 {117}
S: Subject: test
S: From: Alan Smith <alan@smith.me>
S: To: Alan Smith <alice@example.tld>
S:
S: Hello, world!
S: .
S: )
S: A6 OK FETCH completed
```
## Disconnect
```
C: A7 LOGOUT
S: * BYE Logging out
S: A7 OK Logout completed
```
## Full trace
An (old) IMAP trace extracted from Aerogramme:
```
S: * OK Hello
C: A1 LOGIN alice hunter2
S: A1 OK Completed
C: A2 SELECT INBOX
S: * 0 EXISTS
S: * 0 RECENT
S: * FLAGS (\Seen \Answered \Flagged \Deleted \Draft)
S: * OK [PERMANENTFLAGS (\Seen \Answered \Flagged \Deleted \Draft \*)] Flags permitted
S: * OK [UIDVALIDITY 1] UIDs valid
S: * OK [UIDNEXT 1] Predict next UID
S: A2 OK [READ-WRITE] Select completed
C: A3 NOOP
S: A3 OK NOOP completed.
<---- e-mail arrives through LMTP server ---->
C: A4 NOOP
S: * 1 EXISTS
S: A4 OK NOOP completed.
C: A5 FETCH 1 FULL
S: * 1 FETCH (UID 1 FLAGS () INTERNALDATE "06-Jul-2022 14:46:42 +0000"
RFC822.SIZE 117 ENVELOPE (NIL "test" (("Alan Smith" NIL "alan" "smith.me"))
NIL NIL (("Alan Smith" NIL "alan" "aerogramme.tld")) NIL NIL NIL NIL)
BODY ("TEXT" "test" NIL "test" "test" "test" 1 1))
S: A5 OK FETCH completed
C: A6 FETCH 1 (RFC822)
S: * 1 FETCH (UID 1 RFC822 {117}
S: Subject: test
S: From: Alan Smith <alan@smith.me>
S: To: Alan Smith <alan@aerogramme.tld>
S:
S: Hello, world!
S: .
S: )
S: A6 OK FETCH completed
C: A7 LOGOUT
S: * BYE Logging out
S: A7 OK Logout completed
```

View file

@ -1,10 +0,0 @@
+++
title = "Internals"
weight = 50
sort_by = "weight"
template = "documentation.html"
+++
Internals are document that describe how Aerogramme works internally.
They are currently stored as a "knowledge base" without any proper structure.
Feel free to explore them.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View file

@ -1,102 +0,0 @@
+++
title = "Cryptography & key management"
weight = 20
+++
## Key types
Keys that are used:
- master secret key (for indexes)
- curve25519 public/private key pair (for incoming mail)
## Stored keys
Keys that are stored in K2V under PK `keys`:
- `public`: the public curve25519 key (plain text)
- `salt`: the 32-byte salt `S` used to calculate digests that index keys below
- if a password is used, `password:<truncated(128bit) argon2 digest of password using salt S>`:
- a 32-byte salt `Skey`
- followed a secret box
- that is encrypted with a strong argon2 digest of the password (using the salt `Skey`) and a user secret (see below)
- that contains the master secret key and the curve25519 private key
## User secret
An additionnal secret that is added to the password when deriving the encryption key for the secret box.
This additionnal secret should not be stored in K2V/S3, so that just knowing a user's password isn't enough to be able
to decrypt their mailbox (supposing the attacker has a dump of their K2V/S3 bucket).
This user secret should typically be stored in the LDAP database or just in the configuration file when using
the static login provider.
## Operations pseudo-code
We resume here the key cryptography logic for various operations on the mailbox
### Creating a user account
This logic is run when the mailbox is created.
Two modes are supported: password and certificate.
INITIALIZE(`user_secret`, `password`):
&nbsp;&nbsp; if `"salt"` or `"public"` already exist, BAIL
&nbsp;&nbsp; generate salt `S` (32 random bytes)
&nbsp;&nbsp; generate `public`, `private` (curve25519 keypair)
&nbsp;&nbsp; generate `master` (secretbox secret key)
&nbsp;&nbsp; calculate `digest = argon2_S(password)`
&nbsp;&nbsp; generate salt `Skey` (32 random bytes)
&nbsp;&nbsp; calculate `key = argon2_Skey(user_secret + password)`
&nbsp;&nbsp; serialize `box_contents = (private, master)`
&nbsp;&nbsp; seal box `blob = seal_key(box_contents)`
&nbsp;&nbsp; write `S` at `"salt"`
&nbsp;&nbsp; write `concat(Skey, blob)` at `"password:{hex(digest[..16])}"`
&nbsp;&nbsp; write `public` at `"public"`
INITIALIZE_WITHOUT_PASSWORD(`private`, `master`):
&nbsp;&nbsp; if `"salt"` or `"public"` already exist, BAIL
&nbsp;&nbsp; generate salt `S` (32 random bytes)
&nbsp;&nbsp; write `S` at `"salt"`
&nbsp;&nbsp; calculate `public` the public key associated with `private`
&nbsp;&nbsp; write `public` at `"public"`
### Opening the user's mailboxes (upon login)
OPEN(`user_secret`, `password`):
&nbsp;&nbsp; load `S = read("salt")`
&nbsp;&nbsp; calculate `digest = argon2_S(password)`
&nbsp;&nbsp; load `blob = read("password:{hex(digest[..16])}")`
&nbsp;&nbsp; set `Skey = blob[..32]`
&nbsp;&nbsp; calculate `key = argon2_Skey(user_secret + password)`
&nbsp;&nbsp; open secret box `box_contents = open_key(blob[32..])`
&nbsp;&nbsp; retrieve `master` and `private` from `box_contents`
&nbsp;&nbsp; retrieve `public = read("public")`
OPEN_WITHOUT_PASSWORD(`private`, `master`):
&nbsp;&nbsp; load `public = read("public")`
&nbsp;&nbsp; check that `public` is the correct public key associated with `private`
### Account maintenance
ADD_PASSWORD(`user_secret`, `existing_password`, `new_password`):
&nbsp;&nbsp; load `S = read("salt")`
&nbsp;&nbsp; calculate `digest = argon2_S(existing_password)`
&nbsp;&nbsp; load `blob = read("existing_password:{hex(digest[..16])}")`
&nbsp;&nbsp; set `Skey = blob[..32]`
&nbsp;&nbsp; calculate `key = argon2_Skey(user_secret + existing_password)`
&nbsp;&nbsp; open secret box `box_contents = open_key(blob[32..])`
&nbsp;&nbsp; retrieve `master` and `private` from `box_contents`
&nbsp;&nbsp; calculate `digest_new = argon2_S(new_password)`
&nbsp;&nbsp; generate salt `Skeynew` (32 random bytes)
&nbsp;&nbsp; calculate `key_new = argon2_Skeynew(user_secret + new_password)`
&nbsp;&nbsp; serialize `box_contents_new = (private, master)`
&nbsp;&nbsp; seal box `blob_new = seal_key_new(box_contents_new)`
&nbsp;&nbsp; write `concat(Skeynew, blob_new)` at `"new_password:{hex(digest_new[..16])}"`
REMOVE_PASSWORD(`password`):
&nbsp;&nbsp; load `S = read("salt")`
&nbsp;&nbsp; calculate `digest = argon2_S(existing_password)`
&nbsp;&nbsp; check that `"password:{hex(digest[..16])}"` exists
&nbsp;&nbsp; check that other passwords exist ?? (or not)
&nbsp;&nbsp; delete `"password:{hex(digest[..16])}"`

View file

@ -1,57 +0,0 @@
+++
title = "Data format"
weight = 10
+++
## Raw emails
Raw emails are stored in S3 encrypted.
## Bay(ou)
Checkpoints are stored in S3 at `<path>/checkpoint/<timestamp>`. Example:
```
348 TestMailbox/checkpoint/00000180d77400dc126b16aac546b769
369 TestMailbox/checkpoint/00000180d776e509b68fdc5c376d0abc
357 TestMailbox/checkpoint/00000180d77a7fe68f4f76e3b45aa751
```
Operations are stored in K2V at PK `<path>`, SK `<timestamp>`. Example:
```
TestMailbox 00000180d77400dc126b16aac546b769 RcIsESv7WrjMuHwyI/dvCnkIfy6op5Tiylf0WSnn94aMS2uagl7YeMBwdv09TiSXBpu5nJ5e/9QFSfuEI/NqKrdQkX54MOsnaIGhRb0oqUG3KNaar3BiVSvYvXuzYhk4ii+TUS2Eyd6fCCaNVNM5
TestMailbox 00000180d775f27f5542a13fc21c665e RrTSOup/zO1Ei+QrjBcDLt4vvFSY+WJPBodwY64wy2ftW+Oh3VSArvlO4SAEPmdsx1gt0HPBZYR/OkVWsZpmix1ZLFUmvdib+rjNkorHQW1p+oLVK8tolGrqk4SRwl88cqu466T4vBEpDu7tRbH0
TestMailbox 00000180d775f292b3c8da00718389b4 VAwd8SRycIwsipZW5AcSG+EIYZVWn/Uj/TADbWhb4x5LVMceiRBHWVquY08RgT/lJKdhIcUqBA15bVG3klIg8tLsWJVG784NbsZwdGRczWmngcA=
TestMailbox 00000180d775f29d24842cf375d679e0 /FbXtEwm/bijtvOdqM1XFvKUalQFAOPHp+vF9jZThZn/viY5a6W1PyHeI8kTusF6EsVPAwPHpQyjIv/ghskC0f+zUEsSUhDwQANdwLNqDLAvTA==
TestMailbox 00000180d7768ab1dc01ff504e887c62 W/fF0WitpxJ05yHeOv96BlpGymT1kVOjkIW00t9e6UE7mxkvNflu9cZSCd8PDJd2ymC0sC9bLVFAXKmNZsmCFEEHMQSyrX61qTYo4KFCZMp5zm6fXubaYuurrzjXzfUP/R7kBvICFZlF0daf0SwX
TestMailbox 00000180d7768aba629c7ad6adf25228 IPzYGNsSepCX2AEnee/1Eas9a3c5esPSmrNkvaj4XcFb6Ft2KC8N6ubUR3wB+K0oYCTQym6nhHG5dlAxf6NRu7Rk8YtBTBmSqtGqd6kMZ3bU5b8=
TestMailbox 00000180d7768ac1870cda61784114d4 aaLiaWxfx1mxh6aoKE3xUUfZWhivZ/K7ixabflFDW7FO/qbpvCaa+Y6w4lQemTy6m+leAhXGN+Dbyv2qP20yJ9O4oJF5d3Lz5Iv5uF18OxhVZzw=
TestMailbox 00000180d776e4fb294ccdab2612b406 EtUPrLgEeOyab2QRnSie4I3Me9dDh10UdwWnUKdGa/8ezMJDtiy7XlW+tUfJdqtu6Vj7nduT0emDOXbBZsNwlcmzgYNwuNu3I9AfhZTFWtwLgB+wnAgB/jim82DDrJfLia8kB2eA2ao5jfJ3uMSZ
TestMailbox 00000180d776e501528546d340490291 Lz4Z9wCTk1lZ86lL01urhAan4oHcr1NBqdRe+CDpA51D9IncA5+Fhc8I6knUIh2qQ5/woWgISLAVwzSS+0+TxrYoqxf5FumIQtUJfwDER5La3n0=
TestMailbox 00000180d776e509b68fdc5c376d0abc RUGE2xB3fFX/wRH/p2fHIUa+rMaXSRd7fY9zglw0pRfVPqJfpniOjAe4GHIwGlwbwjtFOwS5a+Q7yr0Wez6QwD+ohhqRFKpbjcFcN7VfMyVAf+k=
TestMailbox 00000180d7784b987a8ad8106dc400c9 K+0LVEtBbTnWNS67jy9DtTvQyd5arovduvu490tLOE2TzVhuVoF4pfvTMTN12bH3KwEAHeDfuwKkKJFqldOywouTYPzEjZFkJzyagHrkl6dfnE5CqmlDv+Vc5TOQRskxjW+wQiZdjU8wGiBiBGYh
TestMailbox 00000180d7784bede69ac3cff2c6b724 XMFY3+b1r1//uolVz80JSI3g/84XCk3Tm7/S0BFv+Qe/Xv3/poLrOvAKEe+GzD2s22j8p/T2RXR/JSZckzgjEZeO0wbPDXVQd94di2Pff7jxAH8=
TestMailbox 00000180d7784bffe2595abe7ed81858 QQZhF+7wSHfikoAp93a+UY/XDIX7TVnnVYOtmQ2XHnDKA2F6snRJCPbYBO4IRHCRfVrjDGi32c41it2C3Mu5PBepabxapsW1rfIV3rlX2lkKHtI=
TestMailbox 00000180d77a7fb3f01dbb147c20cf7f IHOlOa1JI11RUKVvQUq3HQPxiRr4UCeE+pHmL8DtNMkOh62V4spuP0VvvQTJCQcPQ1EQR/QcxZ3s7uHLkrZAHF30BkpUkGqsLBWpnyug/puhdiixWsMyLLb6G90zFjiComUwptnDc/CCXtGEHdSW
TestMailbox 00000180d77a7fbb54b100f521ceb347 Ze4KyyTCgrYbZlXlJSY5hNob8sMXvBAmwIx2cADbX5P0M1IHXwXfloEzvvd6WYOtatFC2GnDSrmQ6RdCfeZ3WV9TZilqa0Fv0XEg48sVyVCcguw=
TestMailbox 00000180d77a7fe68f4f76e3b45aa751 cJJVvvRzTVNKUaIHPCCDY2uY7/HlmkxGgo3ozWBlBSRDeBqU65zgZD3QIPCxa6xaqB/Gc0bQ9BGzfU0cvVmO5jgNeeDnbqqs3oeA2jml/Qv2YO9upApfNQtDT1GiwJ8vrgaIow==
TestMailbox 00000180d8e513d3ea58c679a13178ac Ce5su2YOxNmTzk2dK8SX8V/Uue5uAC7oklEjhesY9wCMqGphhOkdWjzCqq0xOzcb/ZzzZ58t+mTksNSYIU4kddHIHBFPgqIwKthVk2mlUdqYiN/Y2vEGqv+YmtKY+GST/7Ee87ZHpU/5sv0GoXxT
TestMailbox 00000180d8e5145a23f8faee86283900 sp3D8xFZcM9icNlDJXIUDJb3mo6VGD9f1aDHD+4RbPdx6mTYF+qNTsPHKCxHHxT/9NfNe8XPg2+8xYRtm7SXfgERZBDB8ye+Xt3fM1k+wbL6RsaJmDHVECeXeL5KHuITzpI22A==
TestMailbox 00000180d8e51465c38f0585f9bb760e FF0VId2O/bBNzYD5ABWReMs5hHoHwynOoJRKj9vyaUMZ3JykInFmvvRgtCbJBDjTQPwPU8apphKQfwuicO76H7GtZqH009Cbv5l8ZTRJKrmzOQmtjzBQc2eGEUMPfbml5t0GCg==
```
The timestamp of a checkpoint corresponds to the timestamp of the first operation NOT included in the checkpoint.
In other words, to reconstruct the final state:
- find timestamp `<ts>` of last checkpoint
- load checkpoint `<ts>`
- load and apply all operations starting from `<ts>`, included
## UID index
The UID index is an application of the Bayou storage module
used to assign UID numbers to e-mails.
See the [IMAP UID proof](/documentation/design/imap-uid/).

View file

@ -1,236 +0,0 @@
+++
title = "IMAP UID proof"
weight = 40
+++
Aerogramme tolerates mailbox
desynchronization between active sessions. Desynchronization
means that different emails can be assigned the same IMAP UID.
Desynchronization are handled by increasing the IMAP UIDVALIDITY field.
When a session learns an operation in a mailbox that conflicts with is
current view, it rebuilds its view from the operation logs and compute a new UIDVALIDITY.
This document shows that, considering any mailbox state for a given session,
the UIDVALIDITY is increased if a conflict occures. In other words, it proves that clients
will never have a corrupted view.
*Note that changing the IMAPUIDVALIDITY will trigger a full mailbox resynchronization for the user,
so it must be used in last resort. It's a "correctness security net" to ease operations, but in normal conditions,
such desynchronizations should not occure. It will be discussed later in the cookbook.*
## Formal definition of a mailbox
### Notations
- $h$: the hash of a message, $\mathbb{H}$ is the set of hashes
- $i$: the UID of a message $(i \in \mathbb{N})$
- $f$: a flag attributed to a message (it's a string), we write
$\mathbb{F}$ the set of possible flags
- if $M$ is a map (aka a dictionnary), if $x$ has no assigned value in
$M$ we write $M [x] = \bot$ or equivalently $x \not\in M$. If $x$ has a value
in the map we write $x \in M$ and $M [x] \neq \bot$
### State
- A map $I$ such that $I [h]$ is the UID of the message whose hash is
$h$ is the mailbox, or $\bot$ if there is no such message
- A map $F$ such that $F [h]$ is the set of flags attributed to the
message whose hash is $h$
- $v$: the UIDVALIDITY value
- $n$: the UIDNEXT value
- $s$: an internal sequence number that is mostly equal to UIDNEXT but
also grows when mails are deleted
### Operations
- MAIL\_ADD$(h, i)$: the value of $i$ that is put in this operation is
the value of $s$ in the state resulting of all already known operations,
i.e. $s (O_{gen})$ in the notation below where $O_{gen}$ is
the set of all operations known at the time when the MAIL\_ADD is generated.
Moreover, such an operation can only be generated if $I (O_{gen}) [h]
= \bot$, i.e. for a mail $h$ that is not already in the state at
$O_{gen}$.
- MAIL\_DEL$(h)$
- FLAG\_ADD$(h, f)$
- FLAG\_DEL$(h, f)$
## Mailbox handling algorithms
### Add an email
**apply** MAIL\_ADD$(h, i)$:
&nbsp;&nbsp; *if* $i < s$:
&nbsp;&nbsp;&nbsp;&nbsp; $v \leftarrow v + s - i$
&nbsp;&nbsp; *if* $F [h] = \bot$:
&nbsp;&nbsp;&nbsp;&nbsp; $F [h] \leftarrow F_{initial}$
&nbsp;&nbsp;$I [h] \leftarrow s$
&nbsp;&nbsp;$s \leftarrow s + 1$
&nbsp;&nbsp;$n \leftarrow s$
### Delete an email
**apply** MAIL\_DEL$(h)$:
&nbsp;&nbsp; $I [h] \leftarrow \bot$
&nbsp;&nbsp;$F [h] \leftarrow \bot$
&nbsp;&nbsp;$s \leftarrow s + 1$
### Add a flag
**apply** FLAG\_ADD$(h, f)$:
&nbsp;&nbsp; *if* $h \in F$:
&nbsp;&nbsp;&nbsp;&nbsp; $F [h] \leftarrow F [h] \cup \{ f \}$
### Delete a flag
**apply** FLAG\_DEL$(h, f)$:
&nbsp;&nbsp; *if* $h \in F$:
&nbsp;&nbsp;&nbsp;&nbsp; $F [h] \leftarrow F [h] \backslash \{ f \}$
## Formal definition of the mailbox log
The mailbox log is a set of operation:
- $o$ is an operation such as MAIL\_ADD, MAIL\_DEL, etc. $O$ is a set of
operations. Operations embed a timestamp, so a set of operations $O$ can be
written as $O = [o_1, o_2, \ldots, o_n]$ by ordering them by timestamp.
- if $o \in O$, we write $O_{\leqslant o}$, $O_{< o}$, $O_{\geqslant
o}$, $O_{> o}$ the set of items of $O$ that are respectively earlier or
equal, strictly earlier, later or equal, or strictly later than $o$. In
other words, if we write $O = [o_1, \ldots, o_n]$, where $o$ is a certain
$o_i$ in this sequence, then:
$$
\begin{aligned}
O_{\leqslant o} &= \{ o_1, \ldots, o_i \}\\
O_{< o} &= \{ o_1, \ldots, o_{i - 1} \}\\
O_{\geqslant o} &= \{ o_i, \ldots, o_n \}\\
O_{> o} &= \{ o_{i + 1}, \ldots, o_n \}
\end{aligned}
$$
- If $O$ is a set of operations, we write $I (O)$, $F (O)$, $n (O), s
(O)$, and $v (O)$ the values of $I, F, n, s$ and $v$ in the state that
results of applying all of the operations in $O$ in their sorted order. (we
thus write $I (O) [h]$ the value of $I [h]$ in this state)
## Proving that UIDValidity is changed for each conflict
**Hypothesis:**
An operation $o$ can only be in a set $O$ if it was
generated after applying operations of a set $O_{gen}$ such that
$O_{gen} \subset O$ (because causality is respected in how we deliver
operations). Sets of operations that do not respect this property are excluded
from all of the properties, lemmas and proofs below.
**Simplification:** We will now exclude FLAG\_ADD and FLAG\_DEL
operations, as they do not manipulate $n$, $s$ and $v$, and adding them should
have no impact on the properties below.
**Small lemma:** If there are no FLAG\_ADD and FLAG\_DEL operations,
then $s (O) = | O |$. This is easy to see because the possible operations are
only MAIL\_ADD and MAIL\_DEL, and both increment the value of $s$ by 1.
**Definition:** If $o$ is a MAIL\_ADD$(h, i)$ operation, and $O$ is a
set of operations such that $o \in O$, then we define the following value:
$$
C (o, O) = s (O_{< o}) - i
$$
We say that $C (o, O)$ is the *number of conflicts of $o$ in $O$*: it
corresponds to the number of operations that were added before $o$ in $O$ that
were not in $O_{gen}$.
**Property:**
We have that:
$$
v (O) = \sum_{o \in O} C (o, O)
$$
Or in English: $v (O)$ is the sum of the number of conflicts of all of the
MAIL\_ADD operations in $O$. This is easy to see because indeed $v$ is
incremented by $C (o, O)$ for each operation $o \in O$ that is applied.
**Property:**
If $O$ and $O'$ are two sets of operations, and $O \subseteq O'$, then:
$$
\begin{aligned}
\forall o \in O, \qquad C (o, O) \leqslant C (o, O')
\end{aligned}
$$
This is easy to see because $O_{< o} \subseteq O'_{< o} $
and
$C (o, O') - C(o, O) = s (O'\_{< o}) - s (O\_{< o}) = | O'\_{< o} | - | O\_{< o} | \geqslant 0$
**Theorem:**
If $O$ and $O'$ are two sets of operations:
$$
\begin{aligned}
O \subseteq O' & \Rightarrow & v (O) \leqslant v (O')
\end{aligned}
$$
**Proof:**
$$
\begin{aligned}
v (O') &= \sum_{o \in O'} C (o, O')\\
& \geqslant \sum_{o \in O} C (o, O') \qquad \text{(because $O \subseteq
O'$)}\\
& \geqslant \sum_{o \in O} C (o, O) \qquad \text{(because $\forall o \in
O, C (o, O) \leqslant C (o, O')$)}\\
& \geqslant v (O)
\end{aligned}
$$
**Theorem:**
If $O$ and $O'$ are two sets of operations, such that $O \subset O'$,
and if there are two different mails $h$ and $h'$ $(h \neq h')$ such that $I
(O) [h] = I (O') [h']$
then:
$$v (O) < v (O')$$
**Proof:**
We already know that $v (O) \leqslant v (O')$ because of the previous theorem.
We will now look at the sum:
$$
v (O') = \sum_{o \in O'} C (o, O')
$$
and show that there is at least one term in this sum that is strictly larger
than the corresponding term in the other sum:
$$
v (O) = \sum_{o \in O} C (o, O)
$$
Let $o$ be the last MAIL\_ADD{{ katex(body="(h, \_)") }} operation in $O$, i.e. the operation
that gives its definitive UID to mail $h$ in $O$, and similarly $o'$ be the
last MAIL\_ADD$(h', \\_)$ operation in $O'$.
Let us write $I = I (O) [h] = I (O') [h']$
$o$ is the operation at position $I$ in $O$, and $o'$ is the operation at
position $I$ in $O'$. But $o \neq o'$, so if $o$ is not the operation at
position $I$ in $O'$ then it has to be at a later position $I' > I$ in $O'$,
because no operations are removed between $O$ and $O'$, the only possibility
is that some other operations (including $o'$) are added before $o$. Therefore
we have that $C (o, O') > C (o, O)$, i.e. at least one term in the sum above
is strictly larger in the first sum than in the second one. Since all other
terms are greater or equal, we have $v (O') > v (O)$.

View file

@ -1,152 +0,0 @@
+++
title = "Mutation log"
weight = 30
+++
Back to our data structure, we note that one major challenge with this project is to *correctly* handle mutable data.
With our current design, multiple processes can interact with the same mutable data without coordination, and we need a way to detect and solve conflicts.
Directly storing the result in a single k2v key would not work as we have no transaction or lock mechanism, and our state would be always corrupted.
Instead, we choose to record an ordered log of operations, ie. transitions, that each client can use locally to rebuild the state, each transition has its own immutable identifier.
This technique is sometimes referred to as event sourcing.
With this system, we can't have conflict anymore at Garage level, but conflicts at the IMAP level can still occur, like 2 processes assigning the same identifier to different emails.
We thus need a logic to handle these conflicts that is flexible enough to accommodate the application's specific logic.
Our solution is inspired by the work conducted by Terry et al. on [Bayou](https://dl.acm.org/doi/10.1145/224056.224070).
Clients fetch regularly the log from Garage, each entry is ordered by a timestamp and a unique identifier.
One of the 2 conflicting clients will be in the state where it has executed a log entry in the wrong order according to the specified ordering.
This client will need to roll back its changes to reapply the log in the same order as the others, and on conflicts, the same logic will be applied by all the clients to get, in the end, the same state.
## Command definitions
The log is made of a sequence of ordered commands that can be run to get a deterministic state in the end.
We define the following commands:
`FLAG_ADD <email_uuid> <flag>` - Add a flag to the target email
`FLAG_DEL <email_uuid> <flag>` - Remove a flag from a target email
`MAIL_DEL <email_uuid>` - Remove an email
`MAIL_ADD <email_uuid> <uid>` - Register an email in the mailbox with the given identifier
`REMOTE <s3 url>` - Command is not directly stored here, instead it must be fetched from S3, see batching to understand why.
*Note: FLAG commands could be enhanced with a MODSEQ field similar to the uid field for the emails, in order to implement IMAP RFC4551. Adding this field would force us to handle conflicts on flags
the same way as on emails, as MODSEQ must be monotonically incremented but is reset by a uid-validity change. This is out of the scope of this document.*
### A note on UUID
When adding an email to the system, we associate it with a *universally unique identifier* or *UUID.*
We can then reference this email in the rest of the system without fearing a conflict or a race condition are we are confident that this UUID is unique.
We could have used the email hash instead, but we identified some benefits in using UUID.
First, sometimes a mail must be duplicated, because the user received it from 2 different sources, so it is more correct to have 2 entries in the system.
Additionally, UUIDs are smaller and better compressible than a hash, which will lead to better performances.
### Batching commands
Commands that are executed at the same time can be batched together.
Let's imagine a user is deleting its trash containing thousands of emails.
Instead of writing thousands of log lines, we can append them in a single entry.
If this entry becomes big (eg. > 100 commands), we can store it to S3 with the `REMOTE` command.
Batching is important as we want to keep the number of log entries small to be able to fetch them regularly and quickly.
## Fixing conflicts in the operation log
The log is applied in order from the last checkpoint.
To stay in sync, the client regularly asks the server for the last commands.
When the log is applied, our system must enforce the following invariants:
- For all emails e1 and e2 in the log, such as e2.order > e1.order, then e2.uid > e1.uid
- For all emails e1 and e2 in the log, such as e1.uuid == e2.uuid, then e1.order == e2.order
If an invariant is broken, the conflict is solved with the following algorithm and the `uidvalidity` value is increased.
```python
def apply_mail_add(uuid, imap_uid):
if imap_uid < internalseq:
uidvalidity += internalseq - imap_uid
mails.insert(uuid, internalseq, flags=["\Recent"])
internalseq = internalseq + 1
uidnext = internalseq
def apply_mail_del(uuid):
mails.remove(uuid)
internalseq = internalseq + 1
```
A mathematical demonstration in Appendix D. shows that this algorithm indeed guarantees that under the same `uidvalidity`, different e-mails cannot share the same IMAP UID.
To illustrate, let us imagine two processes that have a first operation A in common, and then had a divergent state when one applied an operation B, and another one applied an operation C. For process 1, we have:
```python
# state: uid-validity = 1, uid_next = 1, internalseq = 1
(A) MAIL_ADD x 1
# state: uid-validity = 1, x = 1, uid_next = 2, internalseq = 2
(B) MAIL_ADD y 2
# state: uid-validity = 1, x = 1, y = 2, uid_next = 3, internalseq = 3
```
And for process 2 we have:
```python
# state: uid-validity = 1, uid_next = 1, internalseq = 1
(A) MAIL_ADD x 1
# state: uid-validity = 1, x = 1, uid_next = 2, internalseq = 2
(C) MAIL_ADD z 2
# state: uid-validity = 1, x = 1, z = 2, uid_next = 3, internalseq = 3
```
Suppose that a new client connects to one of the two processes after the conflicting operations have been communicated between them. They may have before connected either to process 1 or to process 2, so they might have observed either mail `y` or mail `z` with UID 2. The only way to make sure that the client will not be confused about mail UIDs is to bump the uidvalidity when the conflict is solved. This is indeed what happens with our algorithm: for both processes, once they have learned of the other's conflicting operation, they will execute the following set of operations and end in a deterministic state:
```python
# state: uid-validity = 1, uid_next = 1, internalseq = 1
(A) MAIL_ADD x 1
# state: uid-validity = 1, x = 1, uid_next = 2, internalseq = 2
(B) MAIL_ADD y 2
# state: uid-validity = 1, x = 1, y = 2, uid_next = 3, internalseq = 3
(C) MAIL_ADD z 2
# conflict detected !
# state: uid-validity = 2, x = 1, y = 2, z = 3, uid_next = 4, internalseq = 4
```
## A computed state for efficient requests
From a data structure perspective, a list of commands is very inefficient to get the current state of the mailbox.
Indeed, we don't want an `O(n)` complexity (where `n` is the number of log commands in the log) each time we want to know how many emails are stored in the mailbox.
<!--To address this issue, we plan to maintain a locally computed (rollbackable) state of the mailbox.-->
To address this issue, and thus query the mailbox efficiently, the MDA keeps an in-memory computed version of the logs, ie. the computed state.
**Mapping IMAP identifiers to email identifiers with B-Tree**
Core features of IMAP are synchronization and listing of emails.
Its associated command is `FETCH`, it has 2 parameters, a range of `uid` (or `seq`) and a filter.
For us, it means that we must be able to efficiently select a range of emails by their identifier, otherwise the user experience will be bad, and compute resources will be wasted.
We identified that by using an ordered map based on a B-Tree, we can satisfy this requirement in an optimal manner.
For example, Rust defines a [BTreeMap](https://doc.rust-lang.org/std/collections/struct.BTreeMap.html) object in its standard library.
We define the following structure for our mailbox:
```rust
struct mailbox {
emails: BTreeMap<ImapUid, (EmailUuid, Flags)>,
flags: BTreeMap<Flag, BTreeMap<ImapUid, EmailUuid>>,
name: String,
uid_next: u32,
uid_validity: u32,
/* other fields */
}
```
This data structure allows us to efficiently select a range of emails by their identifier by walking the tree, allowing the server to be responsive to syncronisation request from clients.
### Checkpoints
Having an in-memory computed state does not solve all the problems of operation on a log only, as 1) bootstrapping a fresh client is expensive as we have to replay possibly thousand of logs, and 2) logs would be kept indefinitely, wasting valuable storage resources.
As a solution to these limitations, the MDA regularly checkpoints the in-memory state. More specifically, it serializes it (eg. with MessagePack), compresses it (eg. with zstd), and then stores it on Garage through the S3 API.
A fresh client would then only have to download the latest checkpoint and the range of logs between the checkpoint and now, allowing swift bootstraping while retaining all of the value of the log model.
Old logs and old checkpoints can be garbage collected after a few days for example as long as 1) the most recent checkpoint remains, 2) that all the logs after this checkpoint remain and 3) that we are confident enough that no log before this checkpoint will appear in the future.

View file

@ -1,59 +0,0 @@
+++
title = "Mailboxes"
weight = 20
+++
IMAP servers, at their root, handle mailboxes.
In this document, we explain the domain logic of IMAP and how we map it to Garage data
with Aerogramme.
## IMAP Domain Logic
The main specification of IMAP is defined in [RFC3501](https://datatracker.ietf.org/doc/html/rfc3501).
It defines 3 main objects: Mailboxes, Emails, and Flags. The following figure depicts how they work together:
![An IMAP mailbox schema](/documentation/internals/mailbox.png)
Emails are stored ordered inside the mailbox, and for legacy reasons, the mailbox assigns 2 identifiers to each email we name `uid` and `seq`.
`seq` is the legacy identifier, it numbers messages in a sequence. Each time an email is deleted, the message numbering will change to keep a continuous sequence without holes.
While this numbering is convenient for interactive operations, it is not efficient to synchronize mail locally and quickly detect missing new emails.
To solve this problem, `uid` identifiers were introduced later. They are monotonically increasing integers that must remain stable across time and sessions: when an email is deleted, its identifier is never reused.
This is what Thunderbird uses for example when it synchronizes its mailboxes.
If this ordering cannot be kept, for example because two independent IMAP daemons were adding an email to the same mailbox at the same time, it is possible to change the ordering as long as we change a value named `uid-validity` to trigger a full resynchronization of all clients. As this operation is expensive, we want to minimize the probability of having to trigger a full resynchronization, but in practice, having this recovery mechanism simplifies the operation of an IMAP server by providing a rather simple solution to rare collision situations.
Flags are tags put on an email, some are defined at the protocol level, like `\Recent`, `\Deleted` or `\Seen`, which can be assigned or removed directly by the IMAP daemon.
Others can be defined arbitrarily by the client, for which the MUA will apply its own logic.
There is no mechanism in RFC3501 to synchronize flags between MUA besides listing the flags of all the emails.
IMAP has many extensions, such as [RFC5465](https://www.rfc-editor.org/rfc/rfc5465.html) or [RFC7162](https://datatracker.ietf.org/doc/html/rfc7162).
They are referred to as capabilities and are [referenced by the IANA](https://www.iana.org/assignments/imap-capabilities/imap-capabilities.xhtml).
For this project, we are aiming to implement only IMAP4rev1 and no extension at all.
## Aerogramme Implementation
From a high-level perspective, we will handle _immutable_ emails differently from _mutable_ mailboxes and flags.
Immutable data can be stored directly on Garage, as we do not fear reading an outdated value.
For mutable data, we cannot store them directly in Garage.
Instead, we choose to store a log of operations. Each client then applies this log of operation locally to rebuild its local state.
During this internals phase, we noted that the S3 API semantic was too limited for us, so we introduced a second API, K2V, to have more flexibility.
K2V is internalsed to store and fetch small values in batches, it uses 2 different keys: one to spread the data on the cluster (`P`), and one to sort linked data on the same node (`S`).
Having data on the same node allows for more efficient queries among this data.
For performance reasons, we plan to introduce 2 optimizations.
First, we store an email summary in K2V that allows fetching multiple entries at once.
Second, we also store checkpoints of the logs in S3 to avoid keeping and replaying all the logs each time a client starts a session.
We have the following data handled by Garage:
![Aerogramme Datatypes](/documentation/internals/aero-states.png)
In Garage, it is important to carefully choose the key(s) that are used to store data to have fast queries, we propose the following model:
![Aerogramme Key Choice](/documentation/internals/aero-states2.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View file

@ -1,64 +0,0 @@
+++
title = "Architecture"
weight = 10
+++
Aerogramme stands at the interface between the Garage storage server, and the user's e-mail client. It provides regular IMAP access on the client-side, and stores encrypted e-mail data on the server-side. Aerogramme also provides an LMTP server interface through which incoming mail can be forwarded by the MTA (e.g. Postfix).
<center>
<img src="/documentation/internals/aero-compo.png" alt="Aerogramme components"/>
<br>
<i>Figure 1: Aerogramme, our IMAP daemon, stores its data encrypted in Garage and provides regular IMAP access to mail clients</i></center>
## Overview of architecture
Figure 2 below shows an overview of Aerogramme's architecture. Each user has a personal Garage bucket in which to store their mailbox contents. We will document below the details of the components that make up Aerogramme, but let us first provide a high-level overview. The two main classes, `User` and `Mailbox`, define how data is stored in this bucket, and provide a high-level interface with primitives such as reading the message index, loading a mail's content, copying, moving, and deleting messages, etc. This mail storage system is supported by two important primitives: a cryptography management system that provides encryption keys for user's data, and a simple log-like database system inspired by Bayou [1] which we have called Bay, that we use to store the index of messages in each mailbox. The mail storage system is made accessible to the outside world by two subsystems: an LMTP server that allows for incoming mail to be received and stored in a user's bucket, in a staging area, and the IMAP server itself which allows full-fledged manipulation of mailbox data by users.
<center>
<img src="/documentation/internals/aero-schema.png" alt="Aerogramme internals"/>
<i>Figure 2: Overview of Aerogramme's architecture and internal data structures for a given user, Alice</i></center>
## Cryptography
Our cryptography module is taking care of: authenticating users against a data source (using their IMAP login and password), returning a set of credentials that allow read/write access to a Garage bucket, as well as a set of secret encryption keys used to encrypt and decrypt data stored in the bucket.
The cryptography module makes use of the user's authentication password as a passphrase to decrypt the user's secret keys, which are stored in the user's bucket in a dedicated K2V section.
This module can use either of two data sources for user authentication:
- LDAP, in which case the password (which is also the passphrase for decrypting the user's secret keys) must match the LDAP password of the user.
- Static, in which case the users are statically declared in Aerogramme's configuration file, and can have any password.
The static authentication source can be used in a deployment scenario shown in Figure 3, where Aerogramme is not running on the side of the service provider, but on the user's device itself. In this case, the user can use any password to encrypt their data in the bucket; the only credentials they need for authentication against the service provider are the S3 and K2V API access keys.
<center>
<img src="/documentation/internals/aero-paranoid.png" alt="user side encryption" />
<br>
<i>Figure 3: alternative deployment of Aerogramme on the user's device: the service provider never gets access to the plaintext data.</i></center>
The cryptography module also has a "public authentication" method, which allows the LMTP module to retrieve only a public key for the user to write incoming messages to the user's bucket but without having access to all of the existing encrypted data.
The cryptography module of Aerogramme is based on standard cryptographic primitives from `libsodium` and follows best practices in the domain.
## Bay, a simplification of Bayou
In our last milestone report, we described how we intended to implement the message index for IMAP mailboxes, based on an eventually-consistent log-like data structure. The principles of this system have been established in Bayou in 1995 [1], allowing users to use a weakly-coordinated datastore to exchange data and solve write conflicts. Bayou is based on a sequential specification, which defines the action that operations in the log have on the shared object's state. To handle concurrent modification, Bayou allows for log entries to be appended in non-sequential order: in case a process reads a log entry that was written earlier by another process, it can rewind its execution of the sequential specification to the point where the newly acquired operation should have been executed, and then execute the log again starting from this point. The challenge then consists in defining a sequential specification that provides the desired semantics for the application. In our last milestone report (milestone 3.A), we described a sequential specification that solves the UID assignment problem in IMAP and proved it correct. We refer the reader to that document for more details.
For milestone 3B, we have implemented our customized version of Bayou, which we call Bay. Bay implements the log-like semantics and the rewind ability of Bayou, however, it makes use of a much simpler data system: Bay is not operating on a relational database that is stored on disk, but simply on a data structure in RAM, for which a full checkpoint is written regularly. We decided against using a complex database as we observed that the expected size of the data structures we would be handling (the message indexes for each mailbox) wouldn't be so big most of the time, and having a full copy in RAM was perfectly acceptable. This allows for a drastic simplification in comparison to the proposal of the original Bayou paper [1]. On the other side, we added encryption in Bay so that both log entries and checkpoints are stored encrypted in Garage using the user's secret key, meaning that a malicious Garage administrator cannot read the content of a user's mailbox index.
## LMTP server and incoming mail handler
To handle incoming mail, we had to add a simple LMTP server to Aerogramme. This server uses the public authentication method of the cryptography module to retrieve a set of public credentials (in particular, a public key for asymmetric encryption) for storing incoming messages. The incoming messages are stored in their raw RFC822 form (encrypted) in a specific folder of the Garage bucket called `incoming/`. When a user logs in with their username and password, at which time Aerogramme can decrypt the user's secret keys, a special process is launched that watches the incoming folder and moves these messages to the `INBOX` folder. This task can only be done by a process that knows the user's secret keys, as it has to modify the mailbox index of the `INBOX` folder, which is encrypted using the user's secret keys. In later versions of Aerogramme, this process would be the perfect place to implement mail filtering logic using user-specified rules. These rules could be stored in a dedicated section of the bucket, again encrypted with the user's secret keys.
To implement the LMTP server, we chose to make use of the `smtp-server` crate from the [Kannader](https://github.com/Ekleog/kannader) project (an MTA written in Rust). The `smtp-server` crate had all of the necessary functionality for building SMTP servers, however, it did not handle LMTP. As LMTP is extremely close to SMTP, we were able to extend the `smtp-server` module to allow it to be used for the implementation of both SMTP and LMTP servers. Our work has been proposed as a [pull request](https://github.com/Ekleog/kannader/pull/178) to be merged back upstream in Kannader, which should be integrated soon.
## IMAP server
The last part that remains to build Aerogramme is to implement the logic behind the IMAP protocol and to link it with the mail storage primitives. We started by implementing a state machine that handled the transitions between the different states in the IMAP protocol: ANONYMOUS (before login), AUTHENTICATED (after login), and SELECTED (once a mailbox has been selected for reading/writing). In the SELECTED state, the IMAP session is linked to a given mailbox of the user. In addition, the IMAP server has to keep track of which updates to the mailbox it has sent (or not) to the client so that it can produce IMAP messages consistent with what the client believes to be in the mailbox. In particular, many IMAP commands make use of mail sequence numbers to identify messages, which are indices in the sorted array of all of the messages in the mailbox. However, if messages are added or removed concurrently, these sequence numbers change: hence we must keep a snapshot of the mailbox's index *as the client knows it*, which is not necessarily the same as what is _actually_ in the mailbox, to generate messages that the client will understand correctly. This snapshot is called a *mailbox view* and is synced regularly with the actual mailbox, at which time the corresponding IMAP updates are sent. This can be done only at specific moments when permitted by the IMAP protocol.
The second part of this task consisted in implementing all of the IMAP protocol commands. Most are relatively straightforward, however, one command, in particular, needed special care: the FETCH command. The FETCH command in the IMAP protocol can return the contents of a message to the client. However, it must also understand precisely the semantics of the content of an e-mail message, as the client can specify very precisely how the message should be returned. For instance, in the case of a multipart message with attachments, the client can emit a FECTH command requesting only a certain attachment of the message to be returned, and not the whole message. To implement such semantics, we have based ourselves on the [`mail-parser`](https://docs.rs/mail-parser/latest/mail_parser/) crate, which can fully parse an RFC822-formatted e-mail message, and also supports some extensions such as MIME. To validate that we were correctly converting the parsed message structure to IMAP messages, we internalsed a test suite composed of several weirdly shaped e-mail messages, whose IMAP structure definition we extracted by taking Dovecot as a reference. We were then able to compare the output of Aerogramme on these messages with the reference consisting in what was returned by Dovecot.
## References
- [1] Terry, D. B., Theimer, M. M., Petersen, K., Demers, A. J., Spreitzer, M. J., & Hauser, C. H. (1995). Managing update conflicts in Bayou, a weakly connected replicated storage system. *ACM SIGOPS Operating Systems Review*, 29(5), 172-182. ([PDF](https://dl.acm.org/doi/pdf/10.1145/224057.224070))

View file

@ -1,34 +0,0 @@
+++
title = "Related work"
weight = 50
+++
## Managing update conflicts in Bayou, a weakly connected replicated storage system
by Terry et al.
*to be written*
## Designing a Planetary-Scale IMAP Service with Conflict-free Replicated Data Types
by Jungnickel et al.
*to be written*
## Technology for Resting Email Encrypted Storage (TREES) used
by Rise Up
*to be written*
## Dovecot obox
*to be written*
## Apache JAMES
*to be written*
## Stalwart IMAP
*to be written*

View file

@ -1,182 +0,0 @@
+++
title = "Quick Start"
weight = 0
sort_by = "weight"
template = "documentation.html"
+++
This quickstart guide will teach you how to run an in-memory instance of Aerogramme,
because it is easier to run than a distributed real-world deployment. The goal here
is that you get familiar with the software, you understand what it does and what it does not,
so you can evaluate it. Once ready for a real deployment, you will find all the information you need in the [cookbook](@/documentation/cookbook/_index.md).
*Enjoy your reading!*
## What is Aerogramme
Aerogramme is a [Message Delivery Agent (MDA)](https://en.wikipedia.org/wiki/Message_delivery_agent) that speaks the [IMAP protocol](https://en.wikipedia.org/wiki/Internet_Message_Access_Protocol).
It makes sure that your mailbox, and thus your emails, are stored safely and stay in sync across all your devices.
<!--It manages a bit more than just your emails, as contacts and calendars support is planned.-->
Aerogramme is not (yet) a [Message Transfer Agent (MTA)](https://en.wikipedia.org/wiki/Message_transfer_agent) as it does not speak the [SMTP protocol](https://fr.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol).
It means you can't send email for example with Aerogramme alone, you need to plug it to a MTA, like [Postfix](https://www.postfix.org/) or [opensmtpd](https://www.opensmtpd.org/).
The connection between Aerogramme and the MTA (Postix, opensmtpd, etc.) is done through a protocol named [LMTP](https://en.wikipedia.org/wiki/Local_Mail_Transfer_Protocol).
## Get a binary
Aerogramme is shipped as a static binary for Linux only on `amd64`, `aarch64` and `armv6` architectures.
For convenience, a docker container is also published both on Docker Hub and on Deuxfleurs' registry.
Also, Aerogramme is an open-source software released under the [EUPL licence](https://commission.europa.eu/content/european-union-public-licence_en), and you can compile it yourself from the source.
To get the latest version of Aerogramme and the precise download information, both for binary and source releases, go to the dedicated download page:
<a
href="/download/"
title="Aerogramme releases"
class="group flex items-center justify-center space-x-1 font-semibold shadow hover:shadow hover:border-none border-none px-2 py-1.5 rounded text-white transition-all duration-500 bg-gradient-to-tl from-aerogramme-blue via-blue-500 to-blue-300 bg-size-200 bg-pos-0 hover:bg-pos-100">
<svg class="w-6 h-6 animate-pulse text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10"></path></svg>
<span class="hidden md:inline text-white">Download</span>
</a>
To check if the binary you got is working, just launch it without any parameter, and an help message should be displayed like this one:
```text
aerogramme 0.2.0
Alex Auvolat <alex@adnab.me>, Quentin Dufour <quentin@dufour.io>
A robust email server
USAGE:
aerogramme [OPTIONS] <SUBCOMMAND>
...
```
If you use Docker, you can get the same result with:
```bash
docker run --network host dxflrs/aerogramme:0.2.0 aerogramme
```
*Using host network is not required for this example, but it will be useful for the following,
as, by default, Aerogramme often binds on localhost, and thus its sockets are not available
outside of the container even if you bind the correct ports.*
## Starting the daemon in development mode
To start an in-memory daemon for evaluation purposes, you don't need any configuration file. Just run:
```bash
aerogramme --dev provider daemon
```
If you use the docker container, you must launch:
```bash
docker run --network host dxflrs/aerogramme:0.2.0 aerogramme --dev provider daemon
```
*Starting from now, docker commands will not be given anymore. We assume that you have understood the pattern
on how to map the documentation commands to Docker.*
Now Aerogramme is listening for IMAP on `[::1]:1143` and for LMTP on port `[::1]:1025`. A test account has been created with username `alice` and password `hunter2`.
<!--
## Checking that the service is listening as intended
*If you don't have socat installed on your computer or are not comfortable with interacting
with low-level tools, you can simply skip this section.*
You can try to connect on both ports with `socat` and see a welcome message from the server (CTRL + D to exit):
```
$ socat - tcp:localhost:1143,crlf
* OK [CAPABILITY MOVE UIDPLUS LIST-STATUS ENABLE CONDSTORE LITERAL+ IDLE IMAP4REV1 UNSELECT] Aerogramme
$ socat - tcp:localhost:1025,crlf
220 example.tld Service ready
```
If you got these outputs, well done, your daemon is correctly started and is working!
-->
## Connecting Thunderbird
Now that we have Aerogramme acting as a Mail Delivery Agent, we want to connect our devices to it.
For this quick start guide, we will use Thunderbird on desktop. On the create account page,
start filling the fields.
**Your full name:** `Alice`
**Email address:** `alice@example.tld`
**Password:** `hunter2`
Then click on **Configure Manually**. New fields will appear, fill them as follow.
**Protocol:** `IMAP`
**Hostname:** `::1`
**Port:** `1143`
**Connection Security:** `None`
**Authentication Method:** `Normal password`
**Username:** `alice`
*Aerogramme is not a SMTP server but Thunderbird will not let you configure an account without a SMTP server.
You can lie to Thunderbird by putting any existing SMTP server (like `smtp.gmail.com`) and choose "No Authentication" for the Authentication method field.*
A screenshot for reference:
![Thunderbird Account Creation Page filled with Aerogramme Quickstart configuration](/images/thunderbird-quickstart.png)
Clic "Done" (or "Re-test" then "Done) and your account should be configured:
![Overview of the mailbox list in Thunderbird](/images/thunderbird-quickstart-mbx.png)
As you can see, Thunderbird detected multiple mailboxes but they are all empty.
You can copy emails from your other mailbox to fill your Aerogramme one, test search, add/remove flags, and so on.
Mail reception will be emulated in the following section.
## Emulate the reception of an email
Remember, Aerogramme is about managing your mailbox, not managing mail transfer.
As this is an Aerogramme specific, the goal here is not to deploy Postfix, opensmtpd or any other MTA software.
Instead we will emulate their action: forwarding the messages they received to Aerogramme through the LMTP protocol.
Python natively supports the LMTP protocol: the following command uses Python to deliver a fake email:
```bash
python - <<EOF
from smtplib import LMTP
with LMTP("::1", port=1025) as lmtp:
lmtp.set_debuglevel(2)
lmtp.sendmail(
"bob@example.tld",
"alice@example.tld",
"From: bob@example.tld\r\nTo: alice@example.tld\r\nSubject: Hello\r\n\r\nWorld\r\n",
)
EOF
```
After running three times this script and clicking on 2 of these 3 emails on Thunderbird,
your mailbox should look like that:
![Overview of Thunderbird inbox](/images/thunderbird-quickstart-emails.png)
## That's all folks!
That's (already) the end of the quickstart section of Aerogramme.
You have learnt:
- What Aerogramme does
- How to download Aerogramme
- How to run Aerogramme
- How to connect Thunderbird to Aerogramme
- How MTA are transferring emails to Aerogramme
If you are considering a real-world deployment now, you will
have many topics to consider, like integration with existing MTA, integration with a service manager, TLS encryption,
user management, auto-discovery, persistent storage, high-availability design, observability, backup, updates, etc.
All these points will be covered in the [Cookbook](@/documentation/cookbook/_index.md), it should be your next stop!
<!--
*Earlier, we started Aerogramme in the provider mode with a test account configured with the transparent flavor.*
-->

View file

@ -1,10 +0,0 @@
+++
title = "Reference"
weight = 30
sort_by = "weight"
template = "documentation.html"
+++
Aerogramme uses multiple TOML files, their syntax is described in the dedicated pages:
- [The main configuration](@/documentation/reference/config.md) that must be passed to the daemon to start it
- [The user list configuration](@/documentation/reference/user-list.md) that is used when you choose to use Aerogramme's integrated user management

View file

@ -1,168 +0,0 @@
+++
title = "Configuration"
weight = 10
+++
Aerogramme can be run in 2 modes: `provider` and `companion`.
`provider` is the "traditional" daemon that is run by the service provider on their servers.
`companion` is an optional, local proxy a user can run on their local computer to have better privacy.
This is an Aerogramme-specific concept that is explained in details in the concept page entitled [Per-user encryption](@/documentation/design/per-user-encryption.md).
This page describe both configurations.
## Provider
As the provider is modular (both the user management and the storage can be configured),
the configuration file is also modular. Depending on the modules you choose,
the configuration will look different. The reference configuration file will thus
be presented modules per modules. If you want to see complete configuration files in context,
please refer to the [cookbook](@/documentation/cookbook/_index.md).
The Aerogramme daemon can be launched in provider mode as follow:
```bash
aerogramme -c aerogramme.toml provider daemon
```
The syntax of the `aerogramme.toml` is described below.
### Common
The common part of the provider daemon configuration:
```toml
role = "Provider"
pid = "/var/run/aerogramme.pid"
[auth]
bind_addr = "[::1]:12345"
[imap_unsecure]
bind_addr="[::1]:143"
[imap]
bind_addr="[::]:993"
certs = "my-certs.pem"
key = "my-key.pem"
[lmtp]
bind_addr="[::1]:1025"
hostname="example.tld"
[users]
user_driver = "<PLACEHOLDER>"
# ... depends on the user driver
```
🔑 `role` - *Required, Enum (String)* - Either `Provider` or `Companion`, here only `Provider` is valid. It's a [poka-yoke](https://en.wikipedia.org/wiki/Poka-yoke) to make
sure people run Aerogramme in the intended mode.
🔑 `pid` - *Optional, Path (String)* - The path to the file where the daemon PID will be stored. It's required to use the `aerogramme provider reload` command.
🗃️ `auth` - *Optional* - A socket implementing the [Dovecot SASL protocol](https://doc.dovecot.org/developer_manual/design/auth_protocol/), it can be used by some SMTP servers like Postfix, Chasquid or Maddy to delegate their authentication.
🔑 `bind_addr` - *Required, Socket (String)* - On which IP address and port the Dovecot SASL protocol should listen. Please note that this protocol is not encrypted, and thus you should only bind to localhost.
🗃️ `imap_unsecure` - *Optional* - The cleartext IMAP configuration block, if not set, the IMAP cleartext service is not started. Be careful, it is dangerous to run IMAP without transport encryption.
🔑 `bind_addr` - *Required, Socket (String)* - On which IP address and port the cleartext IMAP service must bind, can be IPv6 or IPv4 syntax. (Port 143 is reserved for this use).
🗃️ `imap` - *Optional* - The TLS IMAP configuration block, if not set, the IMAP TLS service is not started. This is the recommanded way to expose your IMAP service.
🔑 `bind_addr` - *Required, Socket (String)* - On which IP address and port the IMAP service must bind, can be IPv6 or IPv4 syntax. (Port 993 is reserved for this use).
🔑 `certs` - *Required, Path (String)* - A path to the PEM encoded certificate list
🔑 `key` - *Required, Path (String)* - A path to the PEM encoded private key
🗃️ `lmtp` - *Optional* - The LMTP configuration block, if not set, the LMTP service is not started
🔑 `bind_addr` - *Required, Socket (String)* - On which IP address and port the LMTP service must bind, can be IPv6 or IPv4 syntax.
🗃️ `users` - *Required* - How users must be handled
🔑 `user_driver` - *Required, Enum (String)* - Define which user driver must be used, the rest of the configuration depends on it. Valid values are: `Ldap` and `Static`.
### LDAP user_driver
```toml
# root part, see above...
[users]
user_driver = "Ldap"
ldap_server = "ldap://[::1]:389"
pre_bind_on_login = true
bind_dn = "cn=admin,dc=example,dc=tld"
bind_password = "hunter2"
search_base = "ou=users,dc=example,dc=tld"
username_attr = "uid"
mail_attr = "mail"
crypto_root_attr = "aeroCryptoRoot"
storage_driver = "Garage"
s3_endpoint = "s3.example.tld"
k2v_endpoint = "k2v.example.tld"
aws_region = "garage"
aws_access_key_id_attr = "garageS3AccessKey"
aws_secret_access_key_attr = "garageS3AccessKey"
bucket_attr = "aeroBucket"
default_bucket = "aerogramme"
```
🔑 `user_driver="Ldap"` - to configure user management through LDAP, the `user_driver` value introduced in the previous section must be set to `Ldap`.
🔑 `ldap_server` - *Required, LDAP URI* - The LDAP URI of your LDAP server.
🔑 `pre_bind_on_login` - *Optional, boolean, default to false* - Sometimes users can't login directly on the LDAP server and require a service account that will bind to LDAP and be in charge of checking users' credentials. If this is your case, set it to true.
🔑 `bind_dn` - *Optional, LDAP distinguished name (String)* - The distinguished name of your service account (ignored if `pre_bind_on_login` is not set to true)
🔑 `bind_password` - *Optional, Password (String)* - The password of your service account (ignored if `pre_bind_on_login` is not set to true)
🔑 `search_base` - *Optional, LDAP distinguished name (String)* - Once logged with the service account, where the user entries must be searched. (ignored if `pre_bind_on_login` is not set to true)
🔑 `storage_driver` - *Required, Enum (String)* - For now, only `Garage` is supported as a value (`InMemory` can be used for test purposes only)
🔑 `s3_endpoint` - *Required, String* - The S3 endpoint of the Garage instance
🔑 `k2v_endpoint` - *Required, String* - The K2V endpoint of the Garage instance
🔑 `aws_region` - *Required, String* - Regions are an AWS thing, for example `us-east-1`. If you followed Garage's documentation, you probably configured `garage` as your region.
🔑 `aws_access_key_id_attr` - *Required, String* - The LDAP attribute that will contain user's key id: each user has its own key, its own bucket, and so on.
🔑 `aws_secret_access_key_attr` - *Required, String* - The LDAP attribute that will contain user's secret key.
🔑 `bucket_attr` - *Optional, String* - The LDAP attribute that will contain user's bucket that is dedicated to Aerogramme. Again, each user has its own bucket.
🔑 `default_bucket` - *Optional, String* - If `bucket_attr` is not set, we assume that all users have a bucket named following this parameter (as Garage has a local bucket namespace in addition to the global namespace, so it's possible for different users to have independant buckets with the same name).
### Static user_driver
```toml
# root part, see above...
[users]
user_driver = "Static"
user_list = "/etc/aerogramme/users.toml"
```
🔑 `user_driver="Static"` - to configure user management through LDAP, the `user_driver` value introduced in the previous section must be set to `Ldap`.
🔑 `user_list` - *Required, Path (String)* - the path to the file that will contain the list of your users, the hash of their password, and where their data is stored. The file must exist, but it can be blank.
The syntax of the user list file is described in the dedicated reference page [User List](@/documentation/reference/user-list.md).
## Companion
Compared to the provider configuration, the companion configuration is static, so it is presented
in a single block.
```toml
role = "Companion"
pid = "/var/run/user/1000/aerogramme.pid"
[imap]
bind_addr = "[::1]:1143"
[users]
user_list = "/home/user/.config/aerogramme-users.toml"
```
The parameters explanation is the same as above.
Accounts can be configured with `aerogramme companion account add --login alice --setup.toml`.
The content of the `setup.toml` file should be given by your email service provider.
More information about its content is available in [User List](@/documentation/reference/user-list.md).
## Environment variables
Aerogramme supports some environment variables. A non-exhaustive list is given here that apply to both provider and companion modes.
`AEROGRAMME_CONFIG=aerogramme.toml` - *Path* - The path to the configuration file.
`RUST_LOG=info,aerogramme=debug` - *Logger config* - Configure logger verbosity.

View file

@ -1,98 +0,0 @@
+++
title = "RFC Coverage"
weight = 20
+++
Status code :
- 🔴 FAIL - not supported,
- 🟠 PART - partial support,
- 🟢 OK - acceptable support.
## IMF/MIME
Internet Message Format / Multipurpose Internet Mail Extensions are the data representation of an email: its explains how to encode metadata, like the recipient and the sender, but also the content of the email (HTML, plain text, attachments, etc).
*Aerogramme is built on top of its sister project [Deuxfleurs/eml-codec](https://git.deuxfleurs.fr/Deuxfleurs/eml-codec).*
| RFC | Title | Status | Notes |
|-----|--------|-------|-------|
| [822](https://www.rfc-editor.org/rfc/rfc822) | ARPA INTERNET TEXT MESSAGES | 🟢 OK | ... |
| [2822](https://datatracker.ietf.org/doc/html/rfc2822) | Internet Message Format (2001) | 🟢 OK | ... |
| [5322](https://www.rfc-editor.org/rfc/rfc5322.html) | **Internet Message Format (2008)** | 🟢 OK | ... |
| [2045](https://datatracker.ietf.org/doc/html/rfc2045) | ↳ Multipurpose Internet Mail Extensions (MIME) Part One: Format of Internet Message Bodies | 🟢 OK | ... |
| [2046](https://datatracker.ietf.org/doc/html/rfc2046) | ↳ Multipurpose Internet Mail Extensions (MIME) Part Two: Media Types | 🟢 OK | ... |
| [2047](https://datatracker.ietf.org/doc/html/rfc2047) | ↳ MIME (Multipurpose Internet Mail Extensions) Part Three: Message Header Extensions for Non-ASCII Text | 🟢 OK | ... |
| [2048](https://datatracker.ietf.org/doc/html/rfc2048) | ↳ Multipurpose Internet Mail Extensions (MIME) Part Four: Registration Procedures | 🟢 OK | ... |
| [2049](https://datatracker.ietf.org/doc/html/rfc2049) | ↳ Multipurpose Internet Mail Extensions (MIME) Part Five: Conformance Criteria and Examples | 🟢 OK | ... |
| [6532](https://datatracker.ietf.org/doc/html/rfc6532) | Internationalized Email Headers | 🔴 FAIL | ... |
## IMAP
IMAP is a protocol to remotely access a mailbox.
*Based on [duesee/imap-codec](https://github.com/duesee/imap-codec) and [duesee/imap-flow](https://github.com/duesee/imap-flow).*
| RFC | Title | Status | Notes |
|-----|-------|--------|-------|
| [3501](https://www.rfc-editor.org/rfc/rfc3501) | **INTERNET MESSAGE ACCESS PROTOCOL - VERSION 4rev1** | 🟢 OK | ... |
| [9051](https://www.rfc-editor.org/rfc/rfc9051.html) | **Internet Message Access Protocol (IMAP) - Version 4rev2** | 🟠 PART | ... |
| [2177](https://www.rfc-editor.org/rfc/rfc2177.html) | ↳ IMAP4 IDLE command | 🟢 OK | ... |
| [2342](https://www.rfc-editor.org/rfc/rfc2342.html) | ↳ IMAP4 Namespace | 🔴 FAIL | ... |
| [3516](https://www.rfc-editor.org/rfc/rfc3516.html) | ↳ IMAP4 Binary Content Extension | 🔴 FAIL | ... |
| [3691](https://www.rfc-editor.org/rfc/rfc3691.html) | ↳ Internet Message Access Protocol (IMAP) UNSELECT command | 🟢 OK | ... |
| [4315](https://www.rfc-editor.org/rfc/rfc4315.html) | ↳ Internet Message Access Protocol (IMAP) - UIDPLUS extension | 🟢 OK | ... |
| [4731](https://www.rfc-editor.org/rfc/rfc4731.html) | ↳ IMAP4 Extension to SEARCH Command for Controlling What Kind of Information Is Returned | 🔴 FAIL | ... |
| [4959](https://www.rfc-editor.org/rfc/rfc4959.html) | ↳ IMAP Extension for Simple Authentication and Security Layer (SASL) Initial Client Response | 🔴 FAIL | ... |
| [5161](https://www.rfc-editor.org/rfc/rfc5161.html) | ↳ The IMAP ENABLE Extension | 🟢 OK | ... |
| [5182](https://www.rfc-editor.org/rfc/rfc5182.html) | ↳ IMAP Extension for Referencing the Last SEARCH Result | 🔴 FAIL | ... |
| [5258](https://www.rfc-editor.org/rfc/rfc5258.html) | ↳ Internet Message Access Protocol version 4 - LIST Command Extensions | 🔴 FAIL | ... |
| [5530](https://www.rfc-editor.org/rfc/rfc5530.html) | ↳ IMAP Response Codes | 🟠 PART | ... |
| [5819](https://www.rfc-editor.org/rfc/rfc5819.html) | ↳ IMAP4 Extension for Returning STATUS Information in Extended LIST | 🟢 OK | ... |
| [6154](https://www.rfc-editor.org/rfc/rfc6154.html) | ↳ IMAP LIST Extension for Special-Use Mailboxes | 🟠 PART | ... |
| [6851](https://www.rfc-editor.org/rfc/rfc6851.html) | ↳ Internet Message Access Protocol (IMAP) - MOVE Extension | 🟢 OK | ... |
| [7888](https://www.rfc-editor.org/rfc/rfc7888.html) | ↳ IMAP4 Non-synchronizing Literals | 🟢 OK | ... |
| [5550](https://www.rfc-editor.org/rfc/rfc5550.html) | **Lemonade Message Stores** | 🟠 PART | ... |
| [4551](https://www.rfc-editor.org/rfc/rfc4551.html) | ↳ IMAP Extension for Conditional STORE Operation or Quick Flag Changes Resynchronization | 🟢 OK | ... |
| [5162](https://www.rfc-editor.org/rfc/rfc5162.html) | ↳ IMAP4 Extensions for Quick Mailbox Resynchronization | 🔴 FAIL | ... |
| [5465](https://www.rfc-editor.org/rfc/rfc5465.html) | ↳ The IMAP NOTIFY Extension | 🔴 FAIL | ... |
| [7162](https://www.rfc-editor.org/rfc/rfc7162.html) | ↳ IMAP Extensions: Quick Flag Changes Resynchronization (CONDSTORE) and Quick Mailbox Resynchronization (QRESYNC) | 🟠 PART | ... |
## LMTP
LMTP is a protocol used by a Mail Transfer Agent (MTA, like Postfix) to deliver a mail to a local mailbox handled by a Mail Delivery Agent (MDA, like Dovecot or Aerogramme).
*Based on [Ekleog/kannader](https://github.com/Ekleog/kannader).*
| RFC | Title | Status | Notes |
|-----|-------|--------|-------|
| [2033](https://www.rfc-editor.org/rfc/rfc2033) | Local Mail Transfer Protocol | 🟢 OK | ... |
## CalDAV
CalDAV is a protocol to remotely access a calendar and notes.
| RFC | Title | Status | Notes |
|-----|-------|--------|-------|
| [RFC2445](https://www.rfc-editor.org/rfc/rfc2445.html) | Internet Calendaring and Scheduling Core Object Specification (iCalendar) | 🔴 FAIL | ... |
| [RFC2518](https://www.rfc-editor.org/rfc/rfc2518.html) | HTTP Extensions for Distributed Authoring -- WEBDAV | 🔴 FAIL | ... |
| [RFC4791](https://www.rfc-editor.org/rfc/rfc4791) | Calendaring Extensions to WebDAV (CalDAV) | 🔴 FAIL | ... |
## CardDAV
CarDAV is a protocol to remotely access contacts information.
| RFC | Title | Status | Notes |
|-----|-------|--------|-------|
| [6532](https://www.rfc-editor.org/rfc/rfc6352) | CardDAV: vCard Extensions to Web Distributed Authoring and Versioning (WebDAV) | 🔴 FAIL | ... |
## See also
- [Basecamp/Hey reference on email](https://github.com/basecamp/mail/tree/master/reference)
- [Microsoft reference on IMF](https://learn.microsoft.com/en-us/previous-versions/office/developer/exchange-server-2010/aa563098(v=exchg.140))
- [OCaml Mirage project, mrmime](https://github.com/mirage/mrmime)

View file

@ -1,79 +0,0 @@
+++
title = "User list"
weight = 11
+++
This file is used by Aerogramme to store its users
when you choose to use its internal user management feature.
Aerogramme also supports externalized user management (like [LDAP](@/documentation/reference/config.md)): in this case, this file is not needed.
Aerogramme provides utilities commands to generate the `user_list` file, but you can also generate it manually if you prefer. Both methods will be described here.
## CLI-assisted generation of the file
You can add a user to the `user_list` file with the command:
```bash
aerogramme -c aerogramme.toml provider account add --login alice --setup setup.toml
```
The `setup.toml` must be previously created as follow:
```toml
email_addresses = [ "alice@example.tld", "alice.smith@example.tld" ]
clear_password = "hunter2"
storage_driver = "Garage"
s3_endpoint = "s3.example.tld"
k2v_endpoint = "k2v.example.tld"
aws_region = "garage"
aws_access_key_id = "GK01dfa..."
aws_secret_access_key = "a32f..."
bucket = "aerogramme"
```
🔑 `email_addresses` - *Required, Array of emails (Array of String)* - The email addresses that will be associated to this account. Used by the LMTP service to know to which user an email must be delivered.
🔑 `clear_password` - *Optional, Password (String)* - The clear text password of the user. If not set, it will be interactively asked.
🔑 `storage_driver` - *Required, Enum (String)* - The only option is "Garage" for now (you can also use "InMemory" with nothing more for testing purposes).
🔑 `s3_endpoint` - *Required, String* - The S3 endpoint of the Garage instance
🔑 `k2v_endpoint` - *Required, String* - The K2V endpoint of the Garage instance
🔑 `aws_region` - *Required, String* - Regions are an AWS thing, for example `us-east-1`. If you followed Garage's documentation, you probably configured `garage` as your region.
🔑 `aws_access_key_id` - *Required, String* - The user's access key id
🔑 `aws_secret_access_key` - *Required, String* - The user's secret access key
🔑 `bucket` - *Required, String* - The user's bucket in which Aerogramme must store their data
If your Aerogramme daemon is already running, you must reload it to activate this account:
```bash
aerogramme -c aerogramme.toml provider reload
```
*Of course, restarting it also works.*
The previous `... account add ...` command, under the hood, parsed the existing
`users.toml` file, added it the new account, then reserialized the file with the new information. The generated content is given as an example of the following section.
## Manual edit
Some people might want to generate their configuration from another source of truth (eg. Ansible or NixOS). This page will explain the different options available.
The following file has been generated by the `... account add ...` command.
```toml
[alice]
email_addresses = ["alice@example.tld", "alice.smith@example.tld"]
password = "$argon2id$v=19$m=19456,t=2,p=1$lW1IFw59vyZAgQvyPkCB6w$R4y9T+Zekx6tHpTInsXcOZ0H1/HIJoqckiagJq/292U"
crypto_root = "aero:cryptoroot:pass:t5tC2QiL+A543Lg59FmE4XxmS0cSdOWv3ZD1EeeC8CScgR5feMFJT+KyUpjRzplWTEArwTWZ0Ff0VA+HU+P7sbuKqshm5GnN2x7kqePmqRMfLf/q6XiucJmfcNiGVveyrzsRavbs6Vy2J/HyM/FytZ/4eLnZqH8pERpT5UWJdWQehDQnLpG6OEQRgqowun7m+CqF6A/vKydQUBRzMdvX6UGD2bIHLmhRBqIzOYDJQGxQ"
storage_driver = "Garage"
s3_endpoint = "s3.example.tld"
k2v_endpoint = "k2v.example.tld"
aws_region = "garage"
aws_access_key_id = "GK01dfa..."
aws_secret_access_key = "a32f..."
bucket = "aerogramme"
```
🔑 `email_addresses`, `storage_driver`, `s3_endpoint`, `k2v_endpoint`, `aws_region`, `aws_access_key_id`, `aws_secret_access_key`, `bucket` are the same as above.
🔑 `password` - *Required, String* - To generate a compatible hash, run `aerograme tools password-hash`
🔑 `crypto_root` - *Required, String* - To generate a compatible string, run `aerogramme tools crypto-root new`

1
garage Submodule

@ -0,0 +1 @@
Subproject commit aea8b41728422de4920b67c791b13beb948a3c88

2227
package-lock.json generated Executable file

File diff suppressed because it is too large Load diff

View file

@ -4,7 +4,6 @@
"description": "",
"main": "index.js",
"scripts": {
"build": "NODE_ENV=production npx tailwindcss -i src/input.css -o public/style.css --minify",
"start": "npx tailwindcss -i ./src/input.css -o ./public/style.css --minify --watch"
},
"keywords": [],
@ -12,8 +11,5 @@
"license": "ISC",
"devDependencies": {
"tailwindcss": "^3.0.7"
},
"dependencies": {
"html-validate": "^7.18.0"
}
}

View file

@ -11,7 +11,7 @@ article .page-content {
/** Pre */
article pre {
@apply p-4 rounded-md my-4 border-l-4 border-aerogramme-gray shadow-lg overflow-auto;
@apply p-4 rounded-md my-4 border-l-4 border-garage-gray shadow-lg whitespace-pre-wrap;
}
/** Tables */
@ -39,7 +39,7 @@ article table tbody tr td {
/** Blockquotes */
article blockquote {
@apply my-2 py-2 pl-4 border-l-4 border-aerogramme-blue;
@apply my-2 py-2 pl-4 border-l-4 border-garage-orange;
}
/** Ul */
@ -71,7 +71,7 @@ article ol ul {
}
p > code, p > strong > code, li > code, li > strong > code {
@apply bg-gray-100 text-base py-0.5 px-1 rounded font-semibold text-aerogramme-gray shadow-inner;
@apply bg-gray-100 text-base py-0.5 px-1 rounded font-semibold text-garage-gray shadow-inner;
}
.page-content > h1 {
@ -87,23 +87,19 @@ p > code, p > strong > code, li > code, li > strong > code {
}
.page-content {
@apply text-aerogramme-gray;
@apply text-garage-gray;
}
.page-content a {
@apply font-semibold text-aerogramme-blue border-b border-aerogramme-blue hover:text-blue-500 hover:border-blue-500 transition-all duration-500;
@apply font-semibold text-garage-orange border-b border-garage-orange hover:text-red-500 hover:border-red-500 transition-all duration-500;
}
.page-content img {
@apply inline-block my-8 shadow rounded;
}
.footnote-definition p {
@apply inline-block;
}
.is-active {
@apply text-aerogramme-blue;
@apply text-garage-orange;
}
@layer utilities {
@ -128,10 +124,10 @@ p > code, p > strong > code, li > code, li > strong > code {
display: block;
}
/** Home mailbox bg */
/** Home map */
#mailbox-container {
background-image: url('/images/airmail.jpg');
#map-container {
background-image: url('/images/map.svg');
background-repeat: no-repeat;
background-position: left;
background-size: cover;
@ -139,32 +135,8 @@ p > code, p > strong > code, li > code, li > strong > code {
height: 70vh;
}
/*
#mailbox-container {
height: 40vh;
position: relative;
overflow: hidden;
}
#mailbox-container::before {
content: "";
position: absolute;
width: 800%;
height: 800%;
top: -50%;
left: -400%;
z-index: -1;
background-image: url('/images/airmail.jpg');
background-repeat: repeat;
background-attachment: fixed;
background-size: 500px;
transform: rotate(-30deg);
}
*/
@media screen and (max-width: 1280px) {
#mailbox-container {
#map-container {
background-position: center;
background-attachment: scroll;
height: 50vh;
@ -172,7 +144,7 @@ p > code, p > strong > code, li > code, li > strong > code {
}
@media screen and (max-width: 768px) {
#mailbox-container {
#map-container {
background-position: 25% 50%;
background-size: auto;
background-attachment: scroll;
@ -298,4 +270,4 @@ p > code, p > strong > code, li > code, li > strong > code {
margin: 2.75rem 0;
}
}
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.3 KiB

View file

@ -1,57 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="498.50003pt"
height="572.35236pt"
version="1.1"
viewBox="0 0 498.50003 572.35229"
id="svg9186"
sodipodi:docname="noun-phone-book-4217788.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview9188"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="pt"
showgrid="false" />
<defs
id="defs9152">
<linearGradient
inkscape:collect="always"
id="linearGradient9674">
<stop
style="stop-color:#3b82f6;stop-opacity:1;"
offset="0"
id="stop9670" />
<stop
style="stop-color:#343786;stop-opacity:1;"
offset="1"
id="stop9672" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient9674"
id="linearGradient9676"
x1="126.7506"
y1="23.672178"
x2="599.25018"
y2="566.17218"
gradientUnits="userSpaceOnUse" />
</defs>
<g
id="g9184"
transform="translate(-113.7504,-8.746)">
<path
d="m 555.5002,23.676178 h -341.25 c -11.598,0.01172 -22.719,4.6289 -30.922,12.828 -8.1992,8.2031 -12.816,19.324 -12.828,30.922 v 26.25 h -13.125 c -10.941,0 -21.051,5.8359 -26.523,15.312002 -5.4688,9.4766 -5.4688,21.148 0,30.625 5.4727,9.4766 15.582,15.312 26.523,15.312 h 13.125 v 52.5 h -13.125 c -10.941,0 -21.051,5.8359 -26.523,15.312 -5.4688,9.4766 -5.4688,21.148 0,30.625 5.4727,9.4766 15.582,15.312 26.523,15.312 h 13.125 v 52.5 h -13.125 c -10.941,0 -21.051,5.8359 -26.523,15.312 -5.4688,9.4766 -5.4688,21.148 0,30.625 5.4727,9.4766 15.582,15.312 26.523,15.312 h 13.125 v 43.75 h -13.125 c -10.941,0 -21.051,5.8359 -26.523,15.312 -5.4688,9.4766 -5.4688,21.148 0,30.625 5.4727,9.4766 15.582,15.312 26.523,15.312 h 13.125 v 35 c 0.0117,11.598 4.6289,22.719 12.828,30.922 8.2031,8.1992 19.324,12.816 30.922,12.828 h 341.25 c 11.598,-0.0117 22.719,-4.6289 30.922,-12.828 8.1992,-8.2031 12.816,-19.324 12.828,-30.922 V 67.422178 c -0.0117,-11.598 -4.6289,-22.719 -12.828,-30.922 -8.2031,-8.1992 -19.324,-12.816 -30.922,-12.828 z m -398.12,113.750002 c -7.25,0 -13.125,-5.875 -13.125,-13.125 0,-7.25 5.875,-13.125 13.125,-13.125 h 43.75 c 7.25,0 13.125,5.875 13.125,13.125 0,7.25 -5.875,13.125 -13.125,13.125 z m 0,113.75 c -7.25,0 -13.125,-5.875 -13.125,-13.125 0,-7.25 5.875,-13.125 13.125,-13.125 h 43.75 c 7.25,0 13.125,5.875 13.125,13.125 0,7.25 -5.875,13.125 -13.125,13.125 z m 0,113.75 c -7.25,0 -13.125,-5.875 -13.125,-13.125 0,-7.25 5.875,-13.125 13.125,-13.125 h 43.75 c 7.25,0 13.125,5.875 13.125,13.125 0,7.25 -5.875,13.125 -13.125,13.125 z m 0,105 c -7.25,0 -13.125,-5.875 -13.125,-13.125 0,-7.25 5.875,-13.125 13.125,-13.125 h 43.75 c 7.25,0 13.125,5.875 13.125,13.125 0,7.25 -5.875,13.125 -13.125,13.125 z m 424.38,52.5 c -0.008,6.9609 -2.7773,13.633 -7.6953,18.555 -4.9219,4.918 -11.594,7.6875 -18.555,7.6953 h -341.25 c -6.9609,-0.008 -13.633,-2.7773 -18.555,-7.6953 -4.918,-4.9219 -7.6875,-11.594 -7.6953,-18.555 v -35 h 13.125 c 10.941,0 21.051,-5.8359 26.523,-15.312 5.4688,-9.4766 5.4688,-21.148 0,-30.625 -5.4727,-9.4766 -15.582,-15.312 -26.523,-15.312 h -13.125 v -43.75 h 13.125 c 10.941,0 21.051,-5.8359 26.523,-15.312 5.4688,-9.4766 5.4688,-21.148 0,-30.625 -5.4727,-9.4766 -15.582,-15.312 -26.523,-15.312 h -13.125 v -52.5 h 13.125 c 10.941,0 21.051,-5.8359 26.523,-15.312 5.4688,-9.4766 5.4688,-21.148 0,-30.625 -5.4727,-9.4766 -15.582,-15.312 -26.523,-15.312 h -13.125 v -52.5 h 13.125 c 10.941,0 21.051,-5.8359 26.523,-15.312 5.4688,-9.4766 5.4688,-21.148 0,-30.625 -5.4727,-9.476602 -15.582,-15.312002 -26.523,-15.312002 h -13.125 v -26.25 c 0.008,-6.9609 2.7773,-13.633 7.6953,-18.555 4.9219,-4.918 11.594,-7.6875 18.555,-7.6953 h 341.25 c 6.9609,0.0078 13.633,2.7773 18.555,7.6953 4.918,4.9219 7.6875,11.594 7.6953,18.555 z m -84.477,-217.54 -53.609,-8.4922 v -0.004 c -2.3203,-0.37109 -4.6914,0.20703 -6.582,1.6094 -7.043,5.1914 -12.48,12.273 -15.68,20.422 -29.32,-11.832 -52.566,-35.078 -64.398,-64.402 8.1484,-3.1992 15.227,-8.6328 20.422,-15.68 1.3945,-1.8906 1.9727,-4.2578 1.6055,-6.5781 l -8.4922,-53.609 v -0.004 c -0.36328,-2.3164 -1.6445,-4.3867 -3.5547,-5.75 -9.375,-6.7031 -20.895,-9.707 -32.348,-8.4414 -11.453,1.2656 -22.039,6.7148 -29.723,15.305 -7.6836,8.5898 -11.926,19.711 -11.914,31.238 0.0508,45.578 18.184,89.277 50.414,121.51 32.23,32.23 75.93,50.363 121.51,50.414 11.527,0.0117 22.648,-4.2305 31.238,-11.914 8.5898,-7.6836 14.039,-18.266 15.305,-29.719 1.2695,-11.457 -1.7383,-22.973 -8.4414,-32.348 -1.3594,-1.9102 -3.4336,-3.1914 -5.75,-3.5547 z m -32.352,60.035 c -40.941,-0.0469 -80.191,-16.332 -109.14,-45.281 -28.949,-28.949 -45.234,-68.199 -45.281,-109.14 0.0117,-7.7773 3.1016,-15.23 8.6016,-20.727 5.4961,-5.5 12.949,-8.5898 20.727,-8.6016 4.9102,-0.008 9.7461,1.2344 14.047,3.6055 l 7.2539,45.809 c -4.4688,4.7578 -10.406,7.8711 -16.859,8.8438 -2.5273,0.38281 -4.7578,1.8516 -6.1094,4.0195 -1.3516,2.1641 -1.6914,4.8164 -0.92188,7.2539 13.133,41.691 45.789,74.348 87.48,87.48 2.4375,0.76953 5.0898,0.42969 7.2539,-0.92188 2.168,-1.3516 3.6367,-3.582 4.0195,-6.1094 0.97266,-6.4531 4.0859,-12.391 8.8438,-16.859 l 45.809,7.2539 c 2.3711,4.3008 3.6133,9.1367 3.6055,14.047 -0.0117,7.7773 -3.1016,15.23 -8.6016,20.727 -5.4961,5.5 -12.949,8.5898 -20.727,8.6016 z"
id="path9154"
style="fill:url(#linearGradient9676);fill-opacity:1" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.7 KiB

View file

@ -1,3 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" style="background-color: rgb(255, 255, 255);" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="47px" height="52px" viewBox="-0.5 -0.5 47 52"><defs><style type="text/css">@import url(https://fonts.googleapis.com/css?family=Architects+Daughter);&#xa;</style></defs><g><path d="M 6 13.27 C 12.47 11 19.53 11 26 13.27 C 32.47 15.54 39.53 15.54 46 13.27 L 46 31.14 C 39.53 33.41 32.47 33.41 26 31.14 C 19.53 28.87 12.47 28.87 6 31.14 L 6 51 Z" fill="#f8cecc" stroke="#b85450" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/><path d="M 1 3.27 C 7.47 1 14.53 1 21 3.27 C 27.47 5.54 34.53 5.54 41 3.27 L 41 21.14 C 34.53 23.41 27.47 23.41 21 21.14 C 14.53 18.87 7.47 18.87 1 21.14 L 1 41 Z" fill="#d5e8d4" stroke="#82b366" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/></g></svg>

Before

Width:  |  Height:  |  Size: 994 B

View file

@ -1,57 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="472.5pt"
height="542.49945pt"
version="1.1"
viewBox="0 0 472.5 542.49945"
id="svg172"
sodipodi:docname="noun-mailbox-4217786.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview174"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="pt"
showgrid="false" />
<defs
id="defs138">
<linearGradient
inkscape:collect="always"
id="linearGradient521">
<stop
style="stop-color:#3b82f6;stop-opacity:1;"
offset="0"
id="stop517" />
<stop
style="stop-color:#343786;stop-opacity:1;"
offset="1"
id="stop519" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient521"
id="linearGradient523"
x1="113.7519"
y1="8.7502489"
x2="586.25189"
y2="551.24969"
gradientUnits="userSpaceOnUse" />
</defs>
<g
id="g170"
transform="translate(-113.7519,-8.7502486)">
<path
d="M 490,157.5 H 437.5 V 128.566 L 571.83,78.195 c 3.4102,-1.2852 5.6719,-4.5469 5.6719,-8.1953 0,-3.6484 -2.2617,-6.9102 -5.6719,-8.1953 l -140,-52.5 c -2.6875,-1.0039 -5.6992,-0.62891 -8.0586,1.0039 -2.3594,1.6328 -3.7695,4.3203 -3.7695,7.1914 v 140 h -210 c -25.52,0.0273 -49.984,10.18 -68.027,28.223 -18.043,18.043 -28.195,42.508 -28.223,68.027 v 210 c 0,2.3203 0.92188,4.5469 2.5625,6.1875 1.64062,1.6406 3.8672,2.5625 6.1875,2.5625 h 192.5 v 70 c 0,2.3203 0.92188,4.5469 2.5625,6.1875 1.64062,1.6406 3.8672,2.5625 6.1875,2.5625 h 52.5 c 2.3203,0 4.5469,-0.92188 6.1875,-2.5625 1.6406,-1.64062 2.5625,-3.8672 2.5625,-6.1875 v -70 h 192.5 c 2.3203,0 4.5469,-0.92188 6.1875,-2.5625 1.6406,-1.64062 2.5625,-3.8672 2.5625,-6.1875 v -210 c -0.0273,-25.52 -10.18,-49.984 -28.223,-68.027 -18.043,-18.043 -42.508,-28.195 -68.027,-28.223 z M 437.5,30.12 543.83,69.995 437.5,109.87 Z M 210,455 H 131.25 V 253.75 c 0,-28.133 15.008,-54.133 39.375,-68.199 24.367,-14.066 54.383,-14.066 78.75,0 24.367,14.066 39.375,40.066 39.375,68.199 V 455 Z m 157.5,78.75 h -35 V 472.5 h 35 z M 568.75,455 H 306.25 V 253.75 C 306.22656,222.367 290.891,192.973 265.168,175 h 154.83 v 106.24 c -11.094,2.8633 -20.082,10.977 -24.066,21.719 -3.9844,10.742 -2.457,22.754 4.0859,32.156 6.543,9.4062 17.273,15.016 28.73,15.016 11.457,0 22.188,-5.6094 28.73,-15.016 6.543,-9.4023 8.0703,-21.414 4.0859,-32.156 -3.9844,-10.742 -12.973,-18.855 -24.066,-21.719 V 175 h 52.5 c 20.879,0.0234 40.895,8.3281 55.66,23.09 14.762,14.766 23.066,34.781 23.09,55.66 z m -140,-157.5 c 4.6406,0 9.0938,1.8438 12.375,5.125 3.2812,3.2812 5.125,7.7344 5.125,12.375 0,4.6406 -1.8438,9.0938 -5.125,12.375 -3.2812,3.2812 -7.7344,5.125 -12.375,5.125 -4.6406,0 -9.0938,-1.8438 -12.375,-5.125 -3.2812,-3.2812 -5.125,-7.7344 -5.125,-12.375 0.004,-4.6406 1.8516,-9.0859 5.1328,-12.367 3.2812,-3.2811 7.7266,-5.1289 12.367,-5.1328 z m -175,0 H 157.5 c -4.832,0 -8.75,3.918 -8.75,8.75 v 35 c 0,2.3203 0.92188,4.5469 2.5625,6.1875 1.64062,1.6406 3.8672,2.5625 6.1875,2.5625 h 96.25 c 2.3203,0 4.5469,-0.92188 6.1875,-2.5625 1.6406,-1.64062 2.5625,-3.8672 2.5625,-6.1875 v -35 c 0,-2.3203 -0.92188,-4.5469 -2.5625,-6.1875 -1.64062,-1.6406 -3.8672,-2.5625 -6.1875,-2.5625 z m -8.75,35 H 166.25 V 315 H 245 Z"
id="path140"
style="fill:url(#linearGradient523);fill-opacity:1" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.7 KiB

View file

@ -1,3 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" style="background-color: rgb(255, 255, 255);" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="62px" height="42px" viewBox="-0.5 -0.5 62 42"><defs/><g><path d="M 10 11 L 60 11 L 60 41 L 10 41 Z" fill="#f8cecc" stroke="#b85450" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/><path d="M 10 11 L 35 26 L 60 11" fill="none" stroke="#b85450" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/><path d="M 1 1 L 51 1 L 51 31 L 1 31 Z" fill="#d5e8d4" stroke="#82b366" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/><path d="M 1 1 L 26 16 L 51 1" fill="none" stroke="#82b366" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/></g></svg>

Before

Width:  |  Height:  |  Size: 873 B

View file

@ -1,2 +0,0 @@
<svg id="Calque_1" data-name="Calque 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 657.78 167.13"><defs><style>.cls-1{fill:url(#Sfumatura_senza_nome_277);}.cls-2{fill:#fff;}.cls-3{fill:#5590a1;}</style><linearGradient id="Sfumatura_senza_nome_277" x1="365.94" y1="183.94" x2="132.1" y2="411.34" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#5697ff"/><stop offset="1" stop-color="#6a0041"/></linearGradient></defs><title>Logo-NGIAssure-tag</title><a xlink:href="http://nlnet.nl/assure" xlink:title="NGI Assure"><g>
<path class="cls-1" d="M425,292.31l-15.21-15.13a12.77,12.77,0,0,1-3.78-9.07v-35.5a18.6,18.6,0,0,0-18.54-18.54H110.59a18.59,18.59,0,0,0-18.53,18.54V362.66a18.59,18.59,0,0,0,18.53,18.54H387.45A18.59,18.59,0,0,0,406,362.66V332a12.81,12.81,0,0,1,3.75-9L425,307.72A10.88,10.88,0,0,0,425,292.31Z" transform="translate(-92.06 -214.07)"/><path class="cls-2" d="M363.52,244.17h0a12.37,12.37,0,0,1,12.38,12.37v82.19a12.38,12.38,0,0,1-12.38,12.38h0a12.37,12.37,0,0,1-12.37-12.38V256.54A12.37,12.37,0,0,1,363.52,244.17Z" transform="translate(-92.06 -214.07)"/><path class="cls-2" d="M323.46,296H309.75A12.66,12.66,0,0,0,297,307.51a12.38,12.38,0,0,0,12.35,13.2h0a1.94,1.94,0,0,1,1.86,2.5,10.9,10.9,0,0,1-7.62,7.27,43.23,43.23,0,0,1-11.08,1.38,36.34,36.34,0,0,1-17.95-4.36,31.28,31.28,0,0,1-12.3-12.14,38,38,0,0,1,0-35.6,30.69,30.69,0,0,1,12.37-12.06,37.78,37.78,0,0,1,18.18-4.28,36.28,36.28,0,0,1,21,6.38,11,11,0,0,0,13-.28,10.86,10.86,0,0,0-.53-17.54,53.58,53.58,0,0,0-9.25-4.91,66.32,66.32,0,0,0-25.51-4.73,63.67,63.67,0,0,0-30.25,7.1,52.6,52.6,0,0,0-21,19.71,57.4,57.4,0,0,0,0,57,52.92,52.92,0,0,0,20.85,19.71,62.57,62.57,0,0,0,29.94,7.1,78.39,78.39,0,0,0,23.07-3.51l.23-.07A28.56,28.56,0,0,0,334.34,322V306.84A10.88,10.88,0,0,0,323.46,296Z" transform="translate(-92.06 -214.07)"/><path class="cls-2" d="M220.23,256.39v82.5A12.22,12.22,0,0,1,208,351.11h-2.32a12.22,12.22,0,0,1-9.45-4.47L155.6,297.15a5.08,5.08,0,0,0-9,3.22v38.52a12.22,12.22,0,0,1-12.22,12.22h0a12.22,12.22,0,0,1-12.22-12.22v-82.5a12.22,12.22,0,0,1,12.22-12.22h2.46a12.2,12.2,0,0,1,9.46,4.48l40.49,49.44a5.07,5.07,0,0,0,9-3.21V256.39A12.23,12.23,0,0,1,208,244.17h0A12.22,12.22,0,0,1,220.23,256.39Z" transform="translate(-92.06 -214.07)"/><path class="cls-3" d="M500.43,308H483l-3.32,8.05h-8.92l16.75-37.58h8.59l16.8,37.58h-9.12Zm-2.74-6.61-6-14.38-6,14.38Z" transform="translate(-92.06 -214.07)"/><path class="cls-3" d="M531.27,315.49a20.07,20.07,0,0,1-6.68-3.14l2.95-6.55a19.88,19.88,0,0,0,5.74,2.85,21.47,21.47,0,0,0,6.66,1.07,10.58,10.58,0,0,0,5.48-1.1,3.32,3.32,0,0,0,1.77-2.92,2.79,2.79,0,0,0-1.05-2.23,7.82,7.82,0,0,0-2.68-1.42c-1.09-.36-2.57-.75-4.43-1.18a60.44,60.44,0,0,1-7-2,11.4,11.4,0,0,1-4.7-3.28,8.82,8.82,0,0,1-2-6,10.46,10.46,0,0,1,1.77-5.93,11.87,11.87,0,0,1,5.34-4.22,22,22,0,0,1,8.73-1.55,29,29,0,0,1,7,.85,21.1,21.1,0,0,1,6,2.47l-2.68,6.61a21,21,0,0,0-10.42-3,9.72,9.72,0,0,0-5.39,1.19,3.58,3.58,0,0,0-1.75,3.11,3,3,0,0,0,2,2.87,30.79,30.79,0,0,0,6.14,1.85,61,61,0,0,1,7,2,11.72,11.72,0,0,1,4.7,3.22,8.64,8.64,0,0,1,2,6,10.25,10.25,0,0,1-1.8,5.88,12.15,12.15,0,0,1-5.39,4.22,22.16,22.16,0,0,1-8.75,1.55A30.59,30.59,0,0,1,531.27,315.49Z" transform="translate(-92.06 -214.07)"/><path class="cls-3" d="M576.26,315.49a20,20,0,0,1-6.69-3.14l3-6.55a19.88,19.88,0,0,0,5.74,2.85,21.47,21.47,0,0,0,6.66,1.07,10.57,10.57,0,0,0,5.47-1.1,3.32,3.32,0,0,0,1.77-2.92,2.81,2.81,0,0,0-1-2.23,7.87,7.87,0,0,0-2.69-1.42c-1.09-.36-2.57-.75-4.43-1.18a61.64,61.64,0,0,1-7-2,11.47,11.47,0,0,1-4.7-3.28,8.87,8.87,0,0,1-2-6,10.4,10.4,0,0,1,1.78-5.93,11.87,11.87,0,0,1,5.34-4.22,21.94,21.94,0,0,1,8.72-1.55,29,29,0,0,1,7,.85,21,21,0,0,1,6,2.47l-2.68,6.61a20.93,20.93,0,0,0-10.41-3,9.73,9.73,0,0,0-5.4,1.19,3.59,3.59,0,0,0-1.74,3.11,3,3,0,0,0,2,2.87,30.71,30.71,0,0,0,6.15,1.85,61.64,61.64,0,0,1,7,2,11.79,11.79,0,0,1,4.7,3.22,8.68,8.68,0,0,1,1.95,6,10.32,10.32,0,0,1-1.79,5.88,12.13,12.13,0,0,1-5.4,4.22,22.13,22.13,0,0,1-8.75,1.55A30.52,30.52,0,0,1,576.26,315.49Z" transform="translate(-92.06 -214.07)"/><path class="cls-3" d="M621.67,312.25q-4.49-4.46-4.48-12.73v-21h8.69V299.2q0,10.1,8.38,10.09a7.82,7.82,0,0,0,6.22-2.44c1.44-1.63,2.15-4.18,2.15-7.65V278.48h8.59v21q0,8.28-4.48,12.73t-12.53,4.45Q626.16,316.7,621.67,312.25Z" transform="translate(-92.06 -214.07)"/><path class="cls-3" d="M694.49,316.06l-7.25-10.47h-8v10.47h-8.69V278.48h16.26a21,21,0,0,1,8.67,1.66,12.42,12.42,0,0,1,7.65,12A12.22,12.22,0,0,1,695.4,304l8.43,12.08Zm-2.2-28.8a9.13,9.13,0,0,0-6-1.69h-7.09v13.09h7.09a9.07,9.07,0,0,0,6-1.71,6,6,0,0,0,2-4.84A5.93,5.93,0,0,0,692.29,287.26Z" transform="translate(-92.06 -214.07)"/><path class="cls-3" d="M749.83,309.08v7H720.74V278.48h28.4v7H729.38v8.16h17.45v6.76H729.38v8.7Z" transform="translate(-92.06 -214.07)"/></g></a></svg>

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View file

@ -1,386 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="232" height="244" enable-background="new" version="1.0">
<defs>
<path id="J" d="M-422 184c-30.11111 0-36.13403 7.44423-42.15625 14.875l-55.625 68.625c.57753.30447 1.23679.5 1.9375.5h191.6875c.70071 0 1.35997-.19553 1.9375-.5l-55.625-68.625C-385.86599 191.44423-391.88885 184-422 184Z" enable-background="new"/>
<path id="K" fill="none" stroke="#000" d="M273.21875 117.09375c-32.50839 0-58.875 26.39786-58.875 58.90625s26.36661 58.875 58.875 58.875S332.125 208.50839 332.125 176s-26.39786-58.90625-58.90625-58.90625zm-.25 15.03125c24.06826 0 43.59375 19.55674 43.59375 43.625s-19.52549 43.59375-43.59375 43.59375S229.375 199.81826 229.375 175.75s19.52549-43.625 43.59375-43.625z"/>
</defs>
<defs>
<linearGradient id="H">
<stop offset="0" stop-color="#fff"/>
<stop offset="1" stop-color="#fff" stop-opacity="0"/>
</linearGradient>
<linearGradient id="G">
<stop offset="0" stop-color="#a3a3a3"/>
<stop offset="1"/>
</linearGradient>
<linearGradient id="F">
<stop offset="0" stop-color="#fff" stop-opacity=".27692308"/>
<stop offset="1" stop-color="#fff" stop-opacity="0"/>
</linearGradient>
<linearGradient id="E">
<stop offset="0" stop-color="#fff"/>
<stop offset="1" stop-color="#fff" stop-opacity="0"/>
</linearGradient>
<linearGradient xlink:href="#a" id="I" x1="170.34314" x2="88.372581" y1="238" y2="1.311599" gradientUnits="userSpaceOnUse"/>
<linearGradient id="a">
<stop offset="0" stop-color="#f6f6f5"/>
<stop offset=".19354598" stop-color="#e7e7e7"/>
<stop offset="1" stop-color="#eeeeec"/>
</linearGradient>
<linearGradient id="o">
<stop offset="0" stop-color="#9aa29a"/>
<stop offset="1" stop-color="#b5beb5"/>
</linearGradient>
<linearGradient id="B">
<stop offset="0"/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
<linearGradient id="C">
<stop offset="0" stop-color="#9497b3"/>
<stop offset="1" stop-color="#4c4059"/>
</linearGradient>
<linearGradient id="D">
<stop offset="0" stop-color="#fffffd"/>
<stop offset="1" stop-color="#cbcbc9"/>
</linearGradient>
<linearGradient id="p">
<stop offset="0" stop-color="#838383"/>
<stop offset="1"/>
</linearGradient>
<linearGradient id="k">
<stop offset="0"/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
<linearGradient id="q">
<stop offset="0" stop-color="#fff"/>
<stop offset="1" stop-color="#d3d7cf"/>
</linearGradient>
<linearGradient id="r">
<stop offset="0" stop-color="#fff"/>
<stop offset="1" stop-color="#fff" stop-opacity="0"/>
</linearGradient>
<linearGradient id="s">
<stop offset="0" stop-color="#fff"/>
<stop offset="1" stop-color="#fff" stop-opacity="0"/>
</linearGradient>
<linearGradient id="t">
<stop offset="0" stop-color="#fff"/>
<stop offset="1" stop-color="#fff" stop-opacity="0"/>
</linearGradient>
<linearGradient id="u">
<stop offset="0" stop-color="#fff"/>
<stop offset="1" stop-color="#fff" stop-opacity="0"/>
</linearGradient>
<linearGradient id="v">
<stop offset="0"/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
<linearGradient id="w">
<stop offset="0"/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
<linearGradient id="l">
<stop offset="0"/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
<linearGradient id="x">
<stop offset="0" stop-color="#fff"/>
<stop offset="1" stop-color="#fff" stop-opacity="0"/>
</linearGradient>
<linearGradient id="y">
<stop offset="0" stop-color="#fff"/>
<stop offset=".44760558" stop-color="#e6e6e6"/>
<stop offset=".81267858" stop-color="#cbcbcb"/>
<stop offset="1" stop-color="#dbdbdb"/>
</linearGradient>
<linearGradient id="z">
<stop offset="0" stop-color="#fff"/>
<stop offset="1" stop-color="#fff" stop-opacity="0"/>
</linearGradient>
<linearGradient id="A">
<stop offset="0" stop-color="#fff"/>
<stop offset="1" stop-color="#fff" stop-opacity="0"/>
</linearGradient>
<linearGradient id="m">
<stop offset="0" stop-color="#fff"/>
<stop offset="1" stop-color="#fff" stop-opacity="0"/>
</linearGradient>
<linearGradient id="n">
<stop offset="0"/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
<linearGradient id="b">
<stop offset="0" stop-color="#dbded7"/>
<stop offset=".31756762" stop-color="#eeeeec"/>
<stop offset=".49662164" stop-color="#f3f3f2"/>
<stop offset=".7668919" stop-color="#d3d7cf"/>
<stop offset="1" stop-color="#babdb6"/>
</linearGradient>
<linearGradient id="c">
<stop offset="0" stop-color="#fff"/>
<stop offset="1" stop-color="#babdb6"/>
</linearGradient>
<linearGradient id="d">
<stop offset="0" stop-color="#fff"/>
<stop offset=".0275" stop-color="#eeeeec" stop-opacity="0"/>
<stop offset=".9775" stop-color="#d3d7cf" stop-opacity="0"/>
<stop offset="1" stop-color="#d3d7cf"/>
</linearGradient>
<linearGradient id="e">
<stop offset="0" stop-color="#fff"/>
<stop offset="1" stop-color="#fff" stop-opacity="0"/>
</linearGradient>
<linearGradient id="f">
<stop offset="0"/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
<linearGradient id="g">
<stop offset="0" stop-color="#eeeeec"/>
<stop offset=".5" stop-color="#f4f4f3"/>
<stop offset="1" stop-color="#fbfbfa"/>
</linearGradient>
<linearGradient id="h">
<stop offset="0" stop-color="#c8cdc3"/>
<stop offset=".10637173" stop-color="#eeeeec"/>
<stop offset="1" stop-color="#fbfbfa"/>
</linearGradient>
<linearGradient id="i">
<stop offset="0" stop-color="#fff"/>
<stop offset="1" stop-color="#fff" stop-opacity="0"/>
</linearGradient>
<linearGradient id="j">
<stop offset="0" stop-color="#fff"/>
<stop offset="1" stop-color="#fff" stop-opacity="0"/>
</linearGradient>
<linearGradient xlink:href="#b" id="N" x1="-112" x2="-112" y1="53" y2="201" gradientTransform="translate(260)" gradientUnits="userSpaceOnUse"/>
<linearGradient xlink:href="#c" id="O" x1="-182" x2="-169" y1="189" y2="321" gradientTransform="translate(260)" gradientUnits="userSpaceOnUse"/>
<linearGradient xlink:href="#d" id="P" x1="-212" x2="-12" y1="198.875" y2="198.875" gradientTransform="translate(260)" gradientUnits="userSpaceOnUse"/>
<linearGradient xlink:href="#e" id="Q" x1="-372.5" x2="-372.5" y1="164.82263" y2="129.75" gradientUnits="userSpaceOnUse"/>
<linearGradient xlink:href="#f" id="T" x1="-324" x2="-324" y1="-17.50746" y2="49.516186" gradientUnits="userSpaceOnUse"/>
<linearGradient xlink:href="#g" id="W" x1="-111" x2="-111" y1="262" y2="183.99359" gradientTransform="translate(260)" gradientUnits="userSpaceOnUse"/>
<linearGradient xlink:href="#h" id="X" x1="-111" x2="-111" y1="268.14868" y2="195.98666" gradientTransform="translate(260)" gradientUnits="userSpaceOnUse"/>
<linearGradient xlink:href="#i" id="Y" x1="-445.5" x2="-445.5" y1="237.42877" y2="256.75098" gradientUnits="userSpaceOnUse"/>
<linearGradient xlink:href="#j" id="ab" x1="-393" x2="-336" y1="153" y2="242" gradientUnits="userSpaceOnUse"/>
<linearGradient xlink:href="#k" id="ah" x1="84" x2="-104.06974" y1="200.5" y2="-127.99076" gradientUnits="userSpaceOnUse"/>
<linearGradient xlink:href="#l" id="ax" x1="190.97945" x2="196.44823" y1="52.972717" y2="39.563599" gradientTransform="translate(-1.5 2.52145)" gradientUnits="userSpaceOnUse"/>
<linearGradient xlink:href="#m" id="aF" x1="189.2542" x2="189.2542" y1="23.661938" y2="43.888237" gradientUnits="userSpaceOnUse"/>
<linearGradient xlink:href="#n" id="aG" x1="-393.5" x2="-393.5" y1="53.754749" y2="-74.509018" gradientUnits="userSpaceOnUse"/>
<linearGradient xlink:href="#o" id="aN" x1="8.9156475" x2="9.8855038" y1="37.197018" y2="52.090679" gradientTransform="matrix(1.53747 0 0 .47726 4.995766 7.023296)" gradientUnits="userSpaceOnUse"/>
<linearGradient xlink:href="#p" id="aS" x1="13.037439" x2="19.359053" y1="8.3119221" y2="17.322744" gradientUnits="userSpaceOnUse"/>
<linearGradient xlink:href="#p" id="aT" x1="13.037439" x2="19.359053" y1="-1.5875777" y2="7.423245" gradientUnits="userSpaceOnUse"/>
<linearGradient xlink:href="#p" id="aU" x1="17.987192" x2="24.308804" y1="3.3621745" y2="12.372997" gradientUnits="userSpaceOnUse"/>
<linearGradient xlink:href="#p" id="aV" x1="8.0876923" x2="14.409305" y1="3.3621745" y2="12.372997" gradientUnits="userSpaceOnUse"/>
<radialGradient xlink:href="#q" id="aj" cx="-80.660522" cy="88.982887" r="18" fx="-80.660522" fy="88.982887" gradientTransform="matrix(25.3062 0 0 12.4602 2199.23 -911.49)" gradientUnits="userSpaceOnUse"/>
<radialGradient xlink:href="#r" id="ak" cx="128.59172" cy="116.46977" r="92.902176" fx="128.59172" fy="116.46977" gradientTransform="matrix(3.59674 -.2853 .1769 2.23031 -355.116 -121.578)" gradientUnits="userSpaceOnUse"/>
<radialGradient xlink:href="#s" id="an" cx="36" cy="45.296875" r="84.203125" fx="36" fy="45.296875" gradientTransform="matrix(0 2.74107 -1.87992 0 120.779 -78.8035)" gradientUnits="userSpaceOnUse"/>
<radialGradient xlink:href="#t" id="ao" cx="127.75" cy="261.27338" r="92.25" fx="127.75" fy="261.27338" gradientTransform="matrix(1.66303 0 0 .34037 -84.7016 148.57)" gradientUnits="userSpaceOnUse"/>
<radialGradient xlink:href="#u" id="ap" cx="20.479548" cy="31.230524" r="93" fx="20.479548" fy="31.230524" gradientTransform="matrix(0 1.0485 -3.87807 0 165.487 13.8549)" gradientUnits="userSpaceOnUse"/>
<radialGradient xlink:href="#t" id="aq" cx="127.75" cy="236.25" r="92.25" fx="127.75" fy="236.25" gradientTransform="matrix(1.57088 0 0 1.11586 -53.057 .954809)" gradientUnits="userSpaceOnUse"/>
<radialGradient xlink:href="#v" id="ar" cx="210.95667" cy="38.580303" r="34.234375" fx="210.95667" fy="38.580303" gradientTransform="matrix(1.04422 0 0 .96082 -12.7447 6.34946)" gradientUnits="userSpaceOnUse"/>
<radialGradient xlink:href="#w" id="av" cx="196.35764" cy="44.213333" r="23.76296" fx="196.35764" fy="44.213333" gradientTransform="matrix(1 0 0 .98931 0 .472637)" gradientUnits="userSpaceOnUse"/>
<radialGradient xlink:href="#x" id="az" cx="190.875" cy="49.25" r="23.5" fx="190.875" fy="49.25" gradientTransform="translate(-85.751 -21.5289) scale(1.44729)" gradientUnits="userSpaceOnUse"/>
<radialGradient xlink:href="#y" id="aA" cx="192.25" cy="45.75" r="23.5" fx="192.25" fy="45.75" gradientTransform="matrix(.5972 -.4509 1.70446 2.25746 -.540251 29.1578)" gradientUnits="userSpaceOnUse"/>
<radialGradient xlink:href="#z" id="aB" cx="186.76546" cy="27.786123" r="8.9714146" fx="186.76546" fy="27.786123" gradientTransform="matrix(2.11516 0 0 2.92788 -207.435 -53.6174)" gradientUnits="userSpaceOnUse"/>
<radialGradient xlink:href="#A" id="aD" cx="208.68414" cy="39.762138" r="13.625" fx="208.68414" fy="39.762138" gradientTransform="matrix(.99733 .073 -.11677 1.59551 5.19968 -29.5236)" gradientUnits="userSpaceOnUse"/>
<radialGradient xlink:href="#B" id="aL" cx="6.702713" cy="73.615715" r="7.228416" fx="6.702713" fy="73.615715" gradientTransform="scale(1.90221 .5257)" gradientUnits="userSpaceOnUse"/>
<radialGradient xlink:href="#B" id="aO" cx="6.702713" cy="73.615715" r="7.228416" fx="6.702713" fy="73.615715" gradientTransform="scale(1.90221 .5257)" gradientUnits="userSpaceOnUse"/>
<radialGradient xlink:href="#C" id="aQ" cx="8.7468252" cy="6.8283234" r="29.889715" fx="8.7468252" fy="6.8283234" gradientUnits="userSpaceOnUse"/>
<radialGradient xlink:href="#D" id="aR" cx="11.901996" cy="10.045444" r="29.292715" fx="11.901996" fy="10.045444" gradientUnits="userSpaceOnUse"/>
<radialGradient xlink:href="#E" id="aY" cx="177" cy="139.125" r="49" fx="177" fy="139.125" gradientTransform="translate(-41.112189 -32.314878) scale(1.23227)" gradientUnits="userSpaceOnUse"/>
<radialGradient xlink:href="#E" id="ba" cx="177" cy="208" r="49" fx="177" fy="208" gradientTransform="translate(-41.112189 -32.314878) scale(1.23227)" gradientUnits="userSpaceOnUse"/>
<radialGradient xlink:href="#E" id="bc" cx="177" cy="139.125" r="49" fx="177" fy="139.125" gradientTransform="translate(-41.112189 -32.314878) scale(1.23227)" gradientUnits="userSpaceOnUse"/>
<radialGradient xlink:href="#E" id="be" cx="179.1151" cy="207.61333" r="49" fx="179.1151" fy="207.61333" gradientTransform="translate(-41.112189 -32.314878) scale(1.23227)" gradientUnits="userSpaceOnUse"/>
<radialGradient xlink:href="#F" id="bf" cx="177" cy="209.87154" r="49" fx="177" fy="209.87154" gradientTransform="translate(-41.112189 -32.314878) scale(1.23227)" gradientUnits="userSpaceOnUse"/>
<radialGradient xlink:href="#E" id="bg" cx="177" cy="139.125" r="49" fx="177" fy="139.125" gradientTransform="translate(-41.112189 -32.314878) scale(1.23227)" gradientUnits="userSpaceOnUse"/>
<radialGradient xlink:href="#G" id="bi" cx="282.43289" cy="169.76842" r="12.83613" fx="282.43289" fy="169.76842" gradientTransform="matrix(.0464 -.17225 .56978 .15349 -96.263932 47.397674)" gradientUnits="userSpaceOnUse"/>
<radialGradient xlink:href="#H" id="bj" cx="167.75" cy="190.625" r="6.125" fx="167.75" fy="190.625" gradientTransform="matrix(.76856 .40135 -.66725 1.27775 166.01698 -120.27255)" gradientUnits="userSpaceOnUse"/>
<radialGradient xlink:href="#H" id="bk" cx="151.72693" cy="169.73349" r="6.125" fx="151.72693" fy="169.73349" gradientTransform="matrix(.80066 -.33274 .90418 2.17571 -105.57674 -161.3059)" gradientUnits="userSpaceOnUse"/>
<filter id="ai">
<feGaussianBlur stdDeviation="2.43"/>
</filter>
<filter id="am">
<feGaussianBlur stdDeviation="2.025"/>
</filter>
<filter id="as" width="1.2118762" height="1.2302682" x="-.10593809" y="-.1151341">
<feGaussianBlur stdDeviation="3.0222701"/>
</filter>
<filter id="at" width="1.4827586" height="1.4827586" x="-.24137931" y="-.24137931">
<feGaussianBlur stdDeviation="4.7270115"/>
</filter>
<filter id="aw">
<feGaussianBlur stdDeviation=".27169794"/>
</filter>
<filter id="ay" width="1.262069" height="1.262069" x="-.13103448" y="-.13103448">
<feGaussianBlur stdDeviation="2.566092"/>
</filter>
<filter id="aC">
<feGaussianBlur stdDeviation=".30732727"/>
</filter>
<filter id="aE" width="1.0632914" height="1.2034857" x="-.0316457" y="-.10174286">
<feGaussianBlur stdDeviation=".35931052"/>
</filter>
<filter id="aI">
<feGaussianBlur stdDeviation="2.5676682"/>
</filter>
<filter id="aK" width="1.1715436" height="1.399375" x="-.08577181" y="-.1996875">
<feGaussianBlur stdDeviation="6.9890625"/>
</filter>
<filter id="L" width="1.1744194" height="3.7858069" x="-.08720969" y="-1.3929034">
<feGaussianBlur stdDeviation="7.4125232"/>
</filter>
<filter id="M" width="1.0693276" height="2.1073513" x="-.0346638" y="-.55367559">
<feGaussianBlur stdDeviation="2.9464591"/>
</filter>
<filter id="S">
<feGaussianBlur stdDeviation="1.43625"/>
</filter>
<filter id="V">
<feGaussianBlur stdDeviation="2.795625"/>
</filter>
<filter id="aa">
<feGaussianBlur stdDeviation="1.3978125"/>
</filter>
<filter id="ad">
<feGaussianBlur stdDeviation="1.075"/>
</filter>
<filter id="af">
<feGaussianBlur stdDeviation=".5375"/>
</filter>
<filter id="aP" width="1.065263" height="1.236149" x="-.03263148" y="-.11807443">
<feGaussianBlur stdDeviation=".37390235"/>
</filter>
<filter id="aM" width="1.0913681" height="1.3306084" x="-.04568407" y="-.1653042">
<feGaussianBlur stdDeviation=".52346328"/>
</filter>
<filter id="aX">
<feGaussianBlur stdDeviation="1.1778125"/>
</filter>
<filter id="bb">
<feGaussianBlur stdDeviation=".96"/>
</filter>
<filter id="aZ">
<feGaussianBlur stdDeviation=".96"/>
</filter>
<filter id="bh">
<feGaussianBlur stdDeviation="1.92"/>
</filter>
<filter id="bd" width="1.2348263" height="1.0924447" x="-.11741316" y="-.04622229">
<feGaussianBlur stdDeviation=".18533703"/>
</filter>
<filter id="bm">
<feGaussianBlur stdDeviation="1.6488693"/>
</filter>
<clipPath id="ag" clipPathUnits="userSpaceOnUse">
<path fill="#e00000" d="M94.752307 81.344625 139.3126 135.80441c5.16383-5.91155 8.78641-9.07383 15.98421-10.56043 10.7493-2.22011 28.44922-2.45177 38.90689 1.00771 5.75177 1.90273 9.91484 6.39876 12.66425 9.81344l44.15496-55.427612-38.18377-52.325902-77.78174-3.535534-40.305093 56.568543Z"/>
</clipPath>
<clipPath id="al" clipPathUnits="userSpaceOnUse">
<path fill="url(#I) #000" d="M36 19c-.562656 0-1 .437344-1 1v217c0 .56266.437344 1 1 1h184c.56266 0 1-.43734 1-1V66c0-13.557692-10.85818-25.295672-16.28125-30.71875C199.29568 29.858173 187.5577 19 174 19H36Z"/>
</clipPath>
<clipPath id="aH" clipPathUnits="userSpaceOnUse">
<path d="M-322-132c-30.11111 0-36.13403 7.44423-42.15625 14.875L-422-40V93.84375C-422 96.13997-420.13996 98-417.84375 98h191.6875C-223.86 98-222 96.13998-222 93.84375V-40l-57.84375-77.125C-285.86598-124.55577-291.88885-132-322-132Z" enable-background="new"/>
</clipPath>
<clipPath id="aJ" clipPathUnits="userSpaceOnUse">
<path d="M-422 38c-30.11111 0-36.13403 7.44423-42.15625 14.875L-522 130v133.84375c0 2.29622 1.86004 4.15625 4.15625 4.15625h191.6875c2.29622 0 4.15625-1.86 4.15625-4.15625V130l-57.84375-77.125C-385.86598 45.444228-391.88885 38-422 38Z" enable-background="new"/>
</clipPath>
<clipPath id="R" clipPathUnits="userSpaceOnUse">
<path d="M-442 129.75v134.09375c0 2.29622 1.86 4.15625 4.15625 4.15625h191.6875c2.29622 0 4.15625-1.86 4.15625-4.15625V129.75l-57.84375 71.375C-305.86599 208.55577-311.88885 216-342 216c-30.11111 0-36.13403-7.44423-42.15625-14.875L-442 129.75Z" enable-background="new"/>
</clipPath>
<clipPath id="U" clipPathUnits="userSpaceOnUse">
<path d="M-472-100.25V33.84375C-472 36.13997-470.14 38-467.84375 38h191.6875C-273.86003 38-272 36.13998-272 33.84375V-100.25l-57.84375 71.375C-335.86599-21.44423-341.88885-14-372-14c-30.11111 0-36.13403-7.44423-42.15625-14.875L-472-100.25Z" enable-background="new"/>
</clipPath>
<clipPath id="Z" clipPathUnits="userSpaceOnUse">
<use xlink:href="#J" enable-background="new"/>
</clipPath>
<clipPath id="ac" clipPathUnits="userSpaceOnUse">
<path d="M-382 38c-30.11111 0-36.13403 7.44423-42.15625 14.875L-482 130v133.84375c0 2.29622 1.86 4.15625 4.15625 4.15625h191.6875c2.29622 0 4.15625-1.86 4.15625-4.15625V130l-57.84375-77.125C-345.86598 45.444228-351.88885 38-382 38Z" enable-background="new"/>
</clipPath>
<clipPath id="ae" clipPathUnits="userSpaceOnUse">
<path d="M-412 38c-30.11111 0-36.13403 7.44423-42.15625 14.875L-512 130v133.84375c0 2.29622 1.86 4.15625 4.15625 4.15625h191.6875c2.29622 0 4.15625-1.86 4.15625-4.15625V130l-57.84375-77.125C-375.86598 45.444228-381.88885 38-412 38Z" enable-background="new"/>
</clipPath>
<clipPath id="aW" clipPathUnits="userSpaceOnUse">
<use xlink:href="#K"/>
</clipPath>
<clipPath id="bl" clipPathUnits="userSpaceOnUse">
<path fill="none" stroke="#000" stroke-width="2.13572478" d="M223.5 178.5107249a46.875 46.875 0 1 1-93.75 0 46.875 46.875 0 1 1 93.75 0z" opacity=".8"/>
</clipPath>
<mask id="au" maskUnits="userSpaceOnUse">
<path fill="#fff" d="M36-31c-.562656 0-1 .437344-1 1v217c0 .56266.437344 1 1 1h184c.56266 0 1-.43734 1-1V16c0-13.557692-12.85818-23.295672-18.28125-28.71875C197.29568-18.141827 187.5577-31 174-31H36z"/>
</mask>
</defs>
<g transform="translate(-21.208751 -10.081339) scale(.7852)">
<rect width="203.2" height="11.2" x="48.4" y="260.4" stroke="#000" stroke-linecap="square" stroke-width="1.11558235" enable-background="new" filter="url(#L)" opacity=".3" rx="5.6" ry="5.6" transform="matrix(1.01046 0 0 .50893 -3.5686399 130.37444)"/>
<rect width="203.2" height="11.2" x="48.4" y="260.4" stroke="#000" stroke-linecap="square" stroke-width="1.12312889" filter="url(#M)" opacity=".5" rx="5.6" ry="5.6" transform="matrix(.99692 0 0 .50893 -1.5386204 130.37444)"/>
<path fill="url(#N)" d="M148 38c-30.11111 0-36.13403 7.44423-42.15625 14.875L48 130v133.84375C48 266.13997 49.86004 268 52.15625 268h191.6875c2.29622 0 4.15625-1.86002 4.15625-4.15625V130l-57.84375-77.125C184.13402 45.444228 178.11115 38 148 38Z"/>
<path fill="url(#O)" d="M48 129.75v134.09375C48 266.13997 49.86004 268 52.15625 268h191.6875c2.29622 0 4.15625-1.86002 4.15625-4.15625V129.75l-57.84375 71.375C184.13401 208.55577 178.11115 216 148 216c-30.11111 0-36.13403-7.44423-42.15625-14.875L48 129.75Z" enable-background="new"/>
<path fill="url(#P)" d="M48 129.75v134.09375C48 266.13997 49.86004 268 52.15625 268h191.6875c2.29622 0 4.15625-1.86002 4.15625-4.15625V129.75l-57.84375 71.375C184.13401 208.55577 178.11115 216 148 216c-30.11111 0-36.13403-7.44423-42.15625-14.875L48 129.75Z" enable-background="new"/>
<path fill="none" stroke="url(#Q)" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" d="M-442 129.75h200l-57.84375 71.375C-305.86599 208.55577-311.88885 216-342 216c-30.11111 0-36.13403-7.44423-42.15625-14.875L-442 129.75Z" clip-path="url(#R)" enable-background="new" filter="url(#S)" transform="translate(490)"/>
<path fill="url(#T)" d="M-371-46c-30.11111 0-36.13403 7.44423-42.15625 14.875l-56.625 68.625c.57753.30447 1.23679.5 1.9375.5h191.6875c.70071 0 1.35997-.19553 1.9375-.5l-54.625-68.625C-334.86599-38.55577-340.88885-46-371-46Z" clip-path="url(#U)" enable-background="new" filter="url(#V)" opacity=".4" transform="translate(520 230)"/>
<path fill="url(#W)" d="M148 184c-30.11111 0-36.13403 7.44423-42.15625 14.875l-55.625 68.625c.57753.30447 1.23679.5 1.9375.5h191.6875c.70071 0 1.35997-.19553 1.9375-.5l-55.625-68.625C184.13401 191.44423 178.11115 184 148 184Z" enable-background="new"/>
<path fill="url(#X)" d="M148 184c-30.11111 0-36.13403 7.44423-42.15625 14.875l-55.625 68.625c.57753.30447 1.23679.5 1.9375.5h191.6875c.70071 0 1.35997-.19553 1.9375-.5l-55.625-68.625C184.13401 191.44423 178.11115 184 148 184Z" enable-background="new"/>
<path fill="none" stroke="url(#Y)" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" d="M-422 184c-30.11111 0-36.13403 7.44423-42.15625 14.875l-55.625 68.625c.57753.30447 1.23679.5 1.9375.5h191.6875c.70071 0 1.35997-.19553 1.9375-.5l-55.625-68.625C-385.86599 191.44423-391.88885 184-422 184Z" clip-path="url(#Z)" enable-background="new" filter="url(#aa)" transform="translate(570)"/>
<path fill="none" stroke="url(#ab)" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M-382 38c-30.11111 0-36.13403 7.44423-42.15625 14.875L-482 130v133.84375c0 2.29622 1.86 4.15625 4.15625 4.15625h191.6875c2.29622 0 4.15625-1.86 4.15625-4.15625V130l-57.84375-77.125C-345.86598 45.444228-351.88885 38-382 38Z" clip-path="url(#ac)" enable-background="new" filter="url(#ad)" transform="matrix(1 0 0 .9978 530 .5855263)"/>
<path fill="none" stroke="#777a74" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.27357304" d="M-412 38c-30.11111 0-36.13403 7.44423-42.15625 14.875L-512 130v133.84375c0 2.29622 1.86 4.15625 4.15625 4.15625h191.6875c2.29622 0 4.15625-1.86 4.15625-4.15625V130l-57.84375-77.125C-375.86598 45.444228-381.88885 38-412 38Z" clip-path="url(#ae)" enable-background="new" filter="url(#af)" transform="translate(560)"/>
</g>
<g clip-path="url(#ag)" transform="translate(-78 10.782921)">
<g clip-path="none" transform="matrix(.73718 -.1354 .1354 .73718 290.01157 51.522858)">
<g transform="translate(-705 -71.136236)">
<path fill="url(#ah)" d="M55.87243 47.32759c-.562656 0-1 .437344-1 1l-1.010863 217c0 .56266.437344 1 1 1H240.88329c.56266 0 1-.43734 1-1l-1.01086-171c0-13.557692-10.85818-25.295672-16.28125-30.71875-5.42307-5.423077-17.16105-16.28125-30.71875-16.28125h-138z" filter="url(#ai)" opacity=".7" transform="matrix(.98925 0 0 1.0129 401.717 .530498)"/>
<g transform="matrix(.98925 0 0 1.02317 401.717 .0442553)">
<g transform="translate(19.8724 27.8276)">
<path fill="url(#aj)" d="M36 19c-.562656 0-1 .437344-1 1v217c0 .56266.437344 1 1 1h184c.56266 0 1-.43734 1-1V66c0-13.557692-10.85818-25.295672-16.28125-30.71875C199.29568 29.858173 187.5577 19 174 19H36Z"/>
<path fill="none" stroke="url(#ak)" stroke-linecap="round" stroke-width=".993967" d="M36.5914 19.703893c-.559039 0-.993572.434533-.993572.993572V236.30255c0 .55904.434533.99357.993572.99357h182.81721c.55904 0 .99357-.43453.99357-.99357V66.401768c0-13.47054-10.78838-25.133066-16.17659-30.521284-5.38821-5.388216-17.05073-16.176591-30.52128-16.176591H36.5914h0z"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-width=".993967" d="M36 19c-.562656 0-1 .437344-1 1v217c0 .56266.437344 1 1 1h184c.56266 0 1-.43734 1-1V66c0-13.557692-10.85818-25.295672-16.28125-30.71875C199.29568 29.858173 187.5577 19 174 19H36Z" clip-path="url(#al)" filter="url(#am)" opacity=".07228908"/>
<path fill="url(#an)" d="M36.625 19.875c-.562656 0-1 .437344-1 1v217c0 .56266.437344 1 1 1v-217c0-.562656.437344-1 1-1h138c11.86299 0 22.34113 8.31989 28.40625 14.03125C198.18503 29.228982 187.23074 19.875 174.625 19.875h-138z"/>
<rect width="184.5" height="51.25" x="35.5" y="186.25" fill="url(#ao)" opacity=".56024098" rx="1.015625" ry="1.015625"/>
</g>
<path fill="url(#ap)" d="M55.875 46.8125c-.562655-.000001-1 .437344-1 1V136h186V93.812499c-.00001-13.557692-10.85818-25.295671-16.28125-30.718749-5.42308-5.423078-17.16105-16.28125-30.71875-16.28125h-138z"/>
<rect width="184.5" height="1.5" x="55.372429" y="263.82758" fill="url(#aq)" rx="1.015625" ry="1.015625"/>
</g>
</g>
<g transform="matrix(.98925 0 0 1.02317 -283.6239 -42.619616)">
<path fill="url(#ar)" d="M162 24c-2.73019 0-5.40625.181079-8.03125.5625C150.7985 30.511615 149 37.292583 149 44.5c0 23.46 19.04 42.5 42.5 42.5 9.77734 0 18.78521-3.319064 25.96875-8.875C216.733 48.128 192.17308 24 162 24Z" filter="url(#as)" opacity=".16867502" transform="matrix(.76048 0 0 .87302 56.1232 8.48265)"/>
<path d="M174-31c13.5577 0 22.3077 11.846154 27.73077 17.269231C207.15384-8.307692 221 2.442308 221 16c0-13.557692-20.11095-8.6028219-26.1915-8.6028219-.6357 0-1.22685-.5176947-1.15022-1.150225C196.16668-14.458158 187.5577-31 174-31Z" filter="url(#at)" mask="url(#au)" opacity=".37951798" transform="translate(0 50)"/>
<path fill="url(#av)" d="M172.59467 20.722269c24.82665-.870834 48.54258 30.545627 47.5 47 0-13.557692-7.49105-16.365385-29.98439-16.365385-.6357 0-1.15022-.51307-1.15022-1.150225 0-22.493344-2.80769-29.48439-16.36539-29.48439z" filter="url(#aw)" opacity=".42771104"/>
<path fill="url(#ax)" d="M171.44822 22.448224c13.5577 0 25.3077 10.846154 30.73077 16.269231 5.42307 5.423077 16.26923 17.173077 16.26923 30.730769 0-13.557692-6.99105-16.365385-29.48439-16.365385-.6357 0-1.15022-.51307-1.15022-1.150225 0-22.493344-2.80769-29.48439-16.36539-29.48439z" filter="url(#ay)" opacity=".27710799"/>
<path fill="url(#az)" d="M173.125 19.75c13.5577 0 25.3077 10.846154 30.73077 16.269231C209.27884 41.442308 220.125 53.192308 220.125 66.75c0-13.557692-6.99105-16.365385-29.48439-16.365385-.6357 0-1.15022-.51307-1.15022-1.150225 0-22.493344-2.80769-29.48439-16.36539-29.48439Z"/>
<path fill="url(#aA)" d="M174 19c13.5577 0 25.3077 10.846154 30.73077 16.269231C210.15384 40.692308 221 52.442308 221 66c0-13.557692-6.99105-16.365385-29.48439-16.365385-.6357 0-1.15022-.51307-1.15022-1.150225C190.36539 25.991046 187.5577 19 174 19Z"/>
<path fill="url(#aB)" d="M174.12505 19.030839c15.48066 1.033686 16.01927 13.642892 16.26345 26.516505-.74413-7.730295 1.374-15.977852-4.24264-22.627417 4.50781 1.501688 9.81111 5.781605 14.93763 9.192388-5.49645-4.866585-14.01539-13.000869-26.95844-13.081476Z" filter="url(#aC)"/>
<path fill="url(#aD)" d="M219.375 57.625c-3.97099-8.9-17.42971-7.296435-27.25-8 9.92758-.23867 24.59801-2.984186 27.25 8z" filter="url(#aE)"/>
<path fill="url(#aF)" d="M174 19c13.5577 0 25.3077 10.846154 30.73077 16.269231C210.15384 40.692308 221 52.442308 221 66c0-13.557692-6.99105-16.365385-29.48439-16.365385-.6357 0-1.15022-.51307-1.15022-1.150225C190.36539 25.991046 187.5577 19 174 19Z"/>
</g>
</g>
<path fill="#888a85" d="M122.63846241 105.67465147 231.6792243 85.35243208l20.70361479 111.08717442-109.04076189 20.3222194zM118.6384561 80.6746525l44.38282592-8.27174637 3.36154791 18.03669857-44.38282592 8.27174636z" opacity=".13461537"/>
<path fill="url(#aG)" d="M-422-40.25V93.84375C-422 96.13997-420.14 98-417.84375 98h191.6875C-223.86003 98-222 96.13998-222 93.84375V-40.25l-55.34375 71.375C-283.36599 38.55577-289.38885 46-319.5 46c-30.11111 0-36.13403-7.44423-42.15625-14.875L-422-40.25Z" clip-path="url(#aH)" enable-background="new" filter="url(#aI)" opacity=".5" transform="matrix(.7852 0 0 .7 425.83199 108.94435)"/>
<use xlink:href="#J" clip-path="url(#aJ)" enable-background="new" filter="url(#aK)" opacity=".4" transform="translate(504.35124 -20.8682) scale(.7852)"/>
</g>
<g transform="translate(-24.514732 11.337005) scale(5.60582)">
<path fill="url(#aL)" d="M26.5 38.7a13.75 3.8 0 0 1-13.75 3.8A13.75 3.8 0 0 1-1 38.7a13.75 3.8 0 0 1 13.75-3.8 13.75 3.8 0 0 1 13.75 3.8z" filter="url(#aM)" opacity=".93303576" transform="matrix(.51404 0 0 .67258 25.70885 10.782564)"/>
<path fill="url(#aN)" d="m29.838359 17.3583-7.068038 8.215839.619508.611861 6.44853-8.8277z"/>
<path fill="#fefefe" d="m29.779094 17.314041-6.303085 8.740435.900446.79501 5.402639-9.535445z"/>
<path fill="url(#aO)" d="M26.5 38.7a13.75 3.8 0 0 1-13.75 3.8A13.75 3.8 0 0 1-1 38.7a13.75 3.8 0 0 1 13.75-3.8 13.75 3.8 0 0 1 13.75 3.8z" filter="url(#aP)" opacity=".3125" transform="matrix(.9 0 0 1 21.1 -2)"/>
<path fill="url(#aQ)" d="M31.160714 16.910715A14.910714 14.910714 0 0 1 16.25 31.821429 14.910714 14.910714 0 0 1 1.3392859 16.910715 14.910714 14.910714 0 0 1 16.25 2a14.910714 14.910714 0 0 1 14.910714 14.910715Z" transform="translate(20.97029 15.85306) scale(.70456)"/>
<path fill="url(#aR)" d="M31.160714 16.910715A14.910714 14.910714 0 0 1 16.25 31.821429 14.910714 14.910714 0 0 1 1.3392859 16.910715 14.910714 14.910714 0 0 1 16.25 2a14.910714 14.910714 0 0 1 14.910714 14.910715Z" transform="translate(23.89626 18.89987) scale(.52164)"/>
<path fill="url(#aS)" d="M17.324117 7.6932044a.61871845.61871845 0 0 1-.618718.6187184.61871845.61871845 0 0 1-.618719-.6187184.61871845.61871845 0 0 1 .618719-.6187184.61871845.61871845 0 0 1 .618718.6187184Z" transform="translate(12.45482 12.63401) scale(1.18917)"/>
<path fill="url(#aT)" d="M17.324117 7.6932044a.61871845.61871845 0 0 1-.618718.6187184.61871845.61871845 0 0 1-.618719-.6187184.61871845.61871845 0 0 1 .618719-.6187184.61871845.61871845 0 0 1 .618718.6187184Z" transform="translate(12.45482 24.40622) scale(1.18917)"/>
<path fill="url(#aU)" d="M17.324117 7.6932044a.61871845.61871845 0 0 1-.618718.6187184.61871845.61871845 0 0 1-.618719-.6187184.61871845.61871845 0 0 1 .618719-.6187184.61871845.61871845 0 0 1 .618718.6187184Z" transform="translate(6.568712 18.52011) scale(1.18917)"/>
<path fill="url(#aV)" d="M17.324117 7.6932044a.61871845.61871845 0 0 1-.618718.6187184.61871845.61871845 0 0 1-.618719-.6187184.61871845.61871845 0 0 1 .618719-.6187184.61871845.61871845 0 0 1 .618718.6187184Z" transform="translate(18.34092 18.52011) scale(1.18917)"/>
<use xlink:href="#K" clip-path="url(#aW)" filter="url(#aX)" transform="translate(-16.230559 -3.6277988) scale(.17839)"/>
<path fill="none" stroke="url(#aY)" stroke-width="2" d="M225 175a48 48 0 0 1-48 48 48 48 0 0 1-48-48 48 48 0 0 1 48-48 48 48 0 0 1 48 48z" filter="url(#aZ)" transform="translate(-3.705301 -7.9090673) scale(.20387)"/>
<path fill="none" stroke="url(#ba)" stroke-width="2" d="M225 175a48 48 0 0 1-48 48 48 48 0 0 1-48-48 48 48 0 0 1 48-48 48 48 0 0 1 48 48z" filter="url(#bb)" opacity=".375" transform="translate(1.4496957 -2.8123191) scale(.17475)"/>
<path fill="none" stroke="url(#bc)" stroke-width="2.08510637" d="M225 175a48 48 0 0 1-48 48 48 48 0 0 1-48-48 48 48 0 0 1 48-48 48 48 0 0 1 48 48z" opacity=".54464285" transform="translate(1.1283471 -3.1300367) scale(.17656)"/>
<g stroke="#000" stroke-linecap="round" filter="url(#bd)" opacity=".45641025" transform="translate(-8e-8 .22298275)">
<path fill="#f3f3f3" stroke-linejoin="round" stroke-width=".62581926" d="M33.39272125 27.65298625A1.08795375 1.08795375 0 0 1 32.3047675 28.74094a1.08795375 1.08795375 0 0 1-1.08795375-1.08795375 1.08795375 1.08795375 0 0 1 1.08795375-1.08795375 1.08795375 1.08795375 0 0 1 1.08795375 1.08795375z"/>
<path fill="none" stroke-width=".62581909" d="m31.823046 26.54761-2.071168-4.630461"/>
<path fill="none" stroke-width=".93872845" d="m29.604314 31.540403 1.921439-2.771874"/>
</g>
<path fill="none" stroke="url(#be)" stroke-width="2.08510637" d="M225 175a48 48 0 0 1-48 48 48 48 0 0 1-48-48 48 48 0 0 1 48-48 48 48 0 0 1 48 48z" opacity=".54464285" transform="translate(2.0940703 -2.1752256) scale(.1711)"/>
<path fill="none" stroke="url(#bf)" stroke-width="2" d="M225 175a48 48 0 0 1-48 48 48 48 0 0 1-48-48 48 48 0 0 1 48-48 48 48 0 0 1 48 48z" filter="url(#aZ)" opacity=".74553576" transform="translate(-3.705301 -7.9090673) scale(.20387)"/>
<path fill="none" stroke="url(#bg)" stroke-width="2" d="M225 175a48 48 0 0 1-48 48 48 48 0 0 1-48-48 48 48 0 0 1 48-48 48 48 0 0 1 48 48z" filter="url(#bh)" transform="translate(-1.9383434 -6.1620753) scale(.19389)"/>
<g transform="translate(17.838619)">
<path fill="url(#bi)" d="M11.848538 21.591547a.31294082.31294082 0 0 0-.217408.440391l1.995695 4.509826c-.336681.254728-.557457.654982-.557457 1.109339 0 .298631.09637.575732.25643.802738a.46941116.46941116 0 0 0-.03902.0446l-1.906502 2.78171a.47238175.47238175 0 1 0 .78044.529584l1.906502-2.776135a.46941116.46941116 0 0 0 .02787-.03902c.118036.0325.245186.05575.373496.05575.769854 0 1.393642-.629363 1.393642-1.399217 0-.769854-.623788-1.393642-1.393642-1.393642-.0906 0-.181774.01134-.267579.02787L12.19416 21.78109a.31294082.31294082 0 0 0-.156088-.156088.31294082.31294082 0 0 0-.189535-.03345zm2.620047 5.279116c.431247 0 .780439.349193.780439.78044 0 .431247-.349192.780439-.780439.780439s-.786014-.349192-.786014-.780439.354767-.78044.786014-.78044z" font-family="Bitstream Vera Sans" style="-inkscape-font-specification:'Bitstream Vera Sans'"/>
<path fill="none" stroke="url(#bj)" d="m172.875 182.875-10.25 15.5" opacity=".8" transform="translate(-17.033298 -3.6277988) scale(.17839)"/>
<path fill="none" stroke="url(#bk)" d="m174.125 167.875-10.875-24.75" opacity=".8" transform="translate(-17.033298 -3.6277988) scale(.17839)"/>
<path fill="none" stroke="#000" stroke-width="2.13572478" d="M223.5 176.375a46.875 46.875 0 0 1-46.875 46.875 46.875 46.875 0 0 1-46.875-46.875 46.875 46.875 0 0 1 46.875-46.875 46.875 46.875 0 0 1 46.875 46.875Z" clip-path="url(#bl)" filter="url(#bm)" opacity=".45128204" transform="matrix(.17086 0 0 -.17086 -15.561728 58.156192)"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.7 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 714 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 32 KiB

5185
static/images/map.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 252 KiB

View file

@ -1,34 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<!-- Created using Karbon14, part of koffice: http://www.koffice.org/karbon -->
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="449px" height="168px">
<defs>
</defs>
<g id="Layer">
</g>
<g id="Layer">
<path fill="#98bf00" d="M446.602 73.8789L449.102 60.234L436.207 60.234L439.957 40.145L424.512 46.191L422.012 60.234L412.617 60.234L410.117 73.8789L419.363 73.8789L416.215 91.1719C416.066 92.125 415.816 93.5234 415.566 95.3203C415.316 97.1211 415.164 98.7188 415.164 100.07C415.215 106.316 416.715 111.465 419.664 115.516C422.613 119.66 427.41 122.109 434.109 122.859L440.555 109.566C437.105 109.117 434.508 107.766 432.66 105.469C430.809 103.117 429.91 100.168 429.91 96.5703C429.91 95.8711 430.012 94.8711 430.16 93.5234C430.309 92.1719 430.461 91.0742 430.609 90.2227L433.609 73.8789L446.602 73.8789L446.602 73.8789Z" />
<path fill="#98bf00" d="M310.707 72.332C313.105 71.4805 315.207 71.0312 316.957 71.0312C318.855 71.0312 320.453 71.582 321.754 72.6797C323.004 73.7305 323.602 75.2812 323.602 77.4297C323.602 78.0273 323.504 78.9297 323.301 80.1797C323.102 81.3281 322.953 82.3789 322.805 83.2773L319.203 100.168C318.953 101.469 318.703 102.82 318.453 104.219C318.203 105.668 318.105 106.918 318.105 107.965C318.105 112.016 319.203 115.414 321.453 118.113C323.602 120.812 327.449 122.41 333 122.859L339.348 110.016C337.195 109.668 335.648 108.867 334.699 107.617C333.699 106.418 333.199 104.719 333.199 102.57C333.199 102.07 333.25 101.469 333.348 100.82C333.398 100.168 333.5 99.6211 333.547 99.2188L337.195 82.0273C337.496 80.5781 337.746 79.1289 337.945 77.6797C338.148 76.2812 338.246 74.8789 338.246 73.5312C338.246 68.582 336.797 64.586 333.898 61.637C330.949 58.688 326.852 57.188 321.602 57.188C318.555 57.188 315.656 57.688 312.809 58.688C310.008 59.637 306.609 61.234 302.66 63.586C302.512 62.637 302.16 61.484 301.66 60.188C301.113 58.938 300.512 57.836 299.863 56.836L286.469 62.586C287.617 64.336 288.516 66.184 289.066 68.082C289.566 69.9805 289.816 71.7812 289.816 73.4297C289.816 74.2812 289.766 75.3281 289.617 76.4805C289.516 77.6289 289.367 78.5273 289.215 79.1797L281.27 121.512L295.664 121.512L304.109 75.8281C306.16 74.2812 308.359 73.1289 310.707 72.332L310.707 72.332Z" />
<path fill="#98bf00" d="M350.742 80.0781C349.191 84.6758 348.441 89.5742 348.441 94.7227C348.441 99.2188 349.043 103.219 350.191 106.719C351.34 110.215 352.992 113.164 355.09 115.516C357.141 117.914 359.688 119.711 362.637 120.961C365.586 122.211 368.883 122.859 372.484 122.859C376.832 122.859 381.129 122.062 385.43 120.461C389.777 118.863 393.574 116.363 396.824 113.016L391.426 100.519C388.926 103.32 386.176 105.418 383.129 106.867C380.078 108.316 377.031 109.016 374.031 109.016C370.535 109.016 367.785 107.918 365.785 105.719C363.836 103.469 362.836 100.668 362.836 97.3711L362.836 96.4219C362.836 96.0234 362.887 95.6211 362.988 95.2227C365.637 94.8711 368.633 94.4219 371.984 93.8242C375.332 93.2227 378.73 92.5234 382.18 91.7227C385.629 90.875 388.977 89.9258 392.273 88.9258C395.523 87.9258 398.422 86.875 400.871 85.8242L400.871 80.0781C400.871 76.5312 400.32 73.332 399.223 70.4805C398.074 67.734 396.574 65.332 394.625 63.285C392.676 61.285 390.324 59.785 387.676 58.785C385.078 57.738 382.23 57.188 379.18 57.188C374.73 57.188 370.582 58.188 366.836 60.137C363.035 62.086 359.789 64.785 357.141 68.2344C354.391 71.6328 352.293 75.5781 350.742 80.0781L350.742 80.0781ZM372.383 69.9805C373.934 69.1328 375.684 68.7344 377.633 68.7344C380.281 68.7344 382.48 69.582 384.227 71.332C385.977 73.0312 386.879 75.5781 386.879 79.0273C385.43 79.4766 383.727 80.0273 381.73 80.5781C379.68 81.0781 377.633 81.5781 375.531 82.0273C373.383 82.4766 371.332 82.9258 369.285 83.3281C367.234 83.6758 365.484 83.9766 363.984 84.2266C364.234 82.1289 364.688 80.1289 365.387 78.2773C366.137 76.4297 367.086 74.7812 368.234 73.3789C369.484 71.9805 370.832 70.832 372.383 69.9805L372.383 69.9805Z" fill-rule="evenodd" />
<path fill="#000000" d="M404.172 140.453C404.172 139.203 403.969 138.055 403.57 137.055C403.172 136.055 402.621 135.207 401.973 134.457C401.27 133.758 400.473 133.207 399.523 132.856C398.574 132.508 397.523 132.309 396.422 132.309C394.973 132.309 393.625 132.606 392.375 133.156C391.125 133.707 390.027 134.508 389.078 135.504C388.125 136.504 387.379 137.656 386.828 139.004C386.277 140.356 385.977 141.805 385.977 143.402C385.977 144.652 386.176 145.75 386.578 146.801C386.926 147.801 387.477 148.652 388.176 149.352C388.828 150.101 389.676 150.648 390.625 151.051C391.574 151.399 392.625 151.598 393.773 151.598C395.176 151.598 396.523 151.301 397.773 150.75C399.023 150.199 400.121 149.398 401.07 148.402C402.02 147.449 402.77 146.25 403.32 144.902C403.871 143.551 404.172 142.055 404.172 140.453L404.172 140.453ZM390.277 140.402C390.574 139.504 390.977 138.703 391.477 138.004C392.023 137.305 392.676 136.754 393.426 136.305C394.176 135.856 394.973 135.656 395.922 135.656C397.371 135.656 398.422 136.106 399.172 137.004C399.922 137.856 400.32 139.106 400.32 140.652C400.32 141.602 400.172 142.555 399.871 143.504C399.621 144.402 399.223 145.203 398.672 145.902C398.121 146.602 397.473 147.152 396.723 147.601C395.973 148 395.125 148.199 394.223 148.199C392.773 148.199 391.727 147.75 390.977 146.902C390.227 146 389.824 144.801 389.824 143.254C389.824 142.305 389.977 141.352 390.277 140.402L390.277 140.402Z" fill-rule="evenodd" />
<path fill="#000000" d="M434.559 132.559L431.008 132.559L429.109 143.602C429.059 143.754 429.012 144.004 429.012 144.352C429.012 144.703 429.012 144.953 429.012 145.203L428.859 145.203L422.465 132.559L419.113 132.559L415.766 151.301L419.363 151.301L421.363 140.004C421.414 139.856 421.414 139.606 421.414 139.356C421.414 139.106 421.414 138.805 421.414 138.504L421.563 138.504L428.109 151.449L431.309 151.149L434.559 132.559L434.559 132.559Z" />
<path fill="#000000" d="M374.383 132.559L370.734 132.559L367.387 151.301L371.082 151.301L374.383 132.559L374.383 132.559Z" />
<path fill="#000000" d="M328.949 132.559L324.703 132.559C323.902 133.906 323.051 135.457 322.102 137.106C321.152 138.754 320.254 140.453 319.355 142.152C318.453 143.852 317.656 145.5 316.906 147.102C316.156 148.699 315.555 150.101 315.105 151.301L318.953 151.301C319.105 150.949 319.254 150.5 319.453 150.051C319.652 149.602 319.855 149.102 320.105 148.652C320.305 148.199 320.504 147.75 320.703 147.301C320.902 146.852 321.102 146.453 321.254 146.102L327.75 146.102C327.801 146.551 327.801 147 327.852 147.5L328 148.949C328.051 149.398 328.102 149.852 328.152 150.301C328.199 150.75 328.199 151.098 328.199 151.449L331.898 151.149C331.898 150.449 331.848 149.648 331.75 148.699C331.699 147.75 331.551 146.75 331.398 145.703C331.25 144.652 331.098 143.504 330.898 142.351C330.75 141.203 330.551 140.055 330.301 138.906C330.102 137.754 329.898 136.656 329.648 135.555C329.398 134.508 329.199 133.508 328.949 132.559L328.949 132.559ZM326.602 138.106C326.703 138.656 326.801 139.254 326.902 139.902C327 140.504 327.102 141.106 327.152 141.652C327.25 142.203 327.301 142.601 327.352 142.953L322.703 142.953C322.953 142.504 323.203 142.004 323.453 141.453C323.754 140.902 324.051 140.305 324.352 139.703C324.703 139.106 325 138.555 325.301 138.004C325.602 137.453 325.852 136.957 326.102 136.606L326.301 136.606C326.402 137.004 326.5 137.504 326.602 138.106L326.602 138.106Z" fill-rule="evenodd" />
<path fill="#000000" d="M357.641 135.957L358.188 132.559L345.395 132.559L344.844 135.957L349.391 135.957L346.742 151.301L350.391 151.301L353.09 135.957L357.641 135.957L357.641 135.957Z" />
<path fill="#000000" d="M297.465 132.309C296.414 132.309 295.363 132.356 294.312 132.457C293.266 132.606 292.266 132.758 291.316 133.008L288.168 150.852C289.117 151.098 290.215 151.25 291.414 151.399C292.566 151.551 293.664 151.598 294.715 151.598C296.262 151.598 297.664 151.348 299.012 150.852C300.363 150.301 301.562 149.602 302.562 148.652C303.559 147.699 304.359 146.551 304.961 145.203C305.508 143.852 305.809 142.305 305.809 140.606C305.809 139.254 305.609 138.106 305.211 137.055C304.762 136.004 304.211 135.156 303.461 134.457C302.711 133.758 301.812 133.207 300.812 132.856C299.762 132.508 298.664 132.309 297.465 132.309L297.465 132.309ZM296.664 135.707C297.414 135.707 298.113 135.805 298.762 135.957C299.414 136.106 299.961 136.406 300.41 136.805C300.91 137.203 301.312 137.703 301.562 138.356C301.812 138.953 301.961 139.703 301.961 140.652C301.961 141.852 301.812 142.902 301.461 143.852C301.16 144.801 300.711 145.602 300.113 146.25C299.512 146.902 298.812 147.352 297.961 147.699C297.113 148.051 296.215 148.199 295.164 148.199C294.715 148.199 294.266 148.199 293.715 148.152C293.164 148.102 292.664 148.051 292.316 148L294.465 135.906C294.766 135.856 295.164 135.805 295.613 135.754C296.062 135.707 296.414 135.707 296.664 135.707L296.664 135.707Z" fill-rule="evenodd" />
<path fill="#000000" d="M185.809 62.586C186.957 64.336 187.855 66.184 188.406 68.082C188.906 69.9805 189.156 71.7812 189.156 73.4297C189.156 74.2812 189.105 75.3281 188.957 76.4805C188.855 77.6289 188.707 78.5273 188.555 79.1797L180.609 121.512L195.004 121.512L203.449 75.8281C205.5 74.2812 207.699 73.1289 210.047 72.332C212.445 71.4805 214.547 71.0312 216.297 71.0312C218.195 71.0312 219.793 71.582 221.094 72.6797C222.344 73.7305 222.941 75.2812 222.941 77.4297C222.941 78.0273 222.844 78.9297 222.645 80.1797C222.441 81.3281 222.293 82.3789 222.145 83.2773L218.543 100.168C218.293 101.469 218.043 102.82 217.793 104.219C217.547 105.668 217.445 106.918 217.445 107.965C217.445 112.016 218.543 115.414 220.793 118.113C222.941 120.812 226.793 122.41 232.34 122.859L238.688 110.016C236.539 109.668 234.988 108.867 234.039 107.617C233.039 106.418 232.539 104.719 232.539 102.57C232.539 102.07 232.59 101.469 232.688 100.82C232.738 100.168 232.84 99.6211 232.891 99.2188L236.539 82.0273C236.836 80.5781 237.086 79.1289 237.285 77.6797C237.488 76.2812 237.586 74.8789 237.586 73.5312C237.586 68.582 236.137 64.586 233.238 61.637C230.289 58.688 226.191 57.188 220.945 57.188C217.895 57.188 214.996 57.688 212.148 58.688C209.348 59.637 205.949 61.234 202 63.586C201.852 62.637 201.5 61.484 201 60.188C200.453 58.938 199.852 57.836 199.203 56.836L185.809 62.586L185.809 62.586Z" />
<path fill="#000000" d="M276.82 31.547L262.676 31.547L251.883 90.0234C251.43 91.9727 251.082 94.0234 250.832 96.1719C250.582 98.2695 250.434 100.219 250.434 102.019C250.434 107.816 251.531 112.566 253.781 116.262C256.031 119.961 259.828 122.16 265.176 122.859L271.672 109.566C270.625 109.066 269.723 108.516 268.875 107.918C268.023 107.367 267.324 106.617 266.773 105.769C266.176 104.918 265.727 103.918 265.477 102.719C265.227 101.519 265.074 100.019 265.074 98.2695C265.074 97.4219 265.125 96.4727 265.227 95.4727C265.375 94.4219 265.527 93.3711 265.676 92.2734L276.82 31.547L276.82 31.547Z" />
<path fill="#000000" d="M246.434 132.559L242.785 132.559L240.387 146.25C239.887 146.801 239.285 147.25 238.535 147.652C237.785 148 236.988 148.199 236.086 148.199C235.188 148.199 234.488 148 233.988 147.601C233.438 147.152 233.188 146.453 233.188 145.402C233.188 145.203 233.238 144.902 233.289 144.504C233.34 144.152 233.34 143.801 233.387 143.504L235.387 132.559L231.688 132.559L229.738 143.453C229.691 143.902 229.641 144.352 229.59 144.801C229.539 145.25 229.539 145.602 229.539 145.953C229.539 146.953 229.691 147.801 229.988 148.551C230.289 149.301 230.691 149.852 231.191 150.301C231.738 150.75 232.34 151.098 232.988 151.301C233.688 151.5 234.387 151.598 235.137 151.598C236.988 151.598 238.637 151.051 240.137 149.898C240.137 150.148 240.137 150.449 240.188 150.75C240.188 151 240.188 151.25 240.234 151.5L243.883 151.25C243.836 151 243.836 150.75 243.836 150.449C243.785 150.199 243.785 149.898 243.785 149.551C243.785 148.949 243.836 148.301 243.883 147.652C243.934 146.953 243.984 146.301 244.133 145.703L246.434 132.559L246.434 132.559Z" />
<path fill="#000000" d="M276.621 132.559L273.074 132.559L271.172 143.602C271.125 143.754 271.074 144.004 271.074 144.352C271.074 144.703 271.074 144.953 271.074 145.203L270.922 145.203L264.527 132.559L261.176 132.559L257.828 151.301L261.426 151.301L263.426 140.004C263.477 139.856 263.477 139.606 263.477 139.356C263.477 139.106 263.477 138.805 263.477 138.504L263.625 138.504L270.176 151.449L273.371 151.149L276.621 132.559L276.621 132.559Z" />
<path fill="#000000" d="M214.797 134.457C214.098 133.758 213.297 133.207 212.348 132.856C211.398 132.508 210.348 132.309 209.25 132.309C207.801 132.309 206.449 132.606 205.199 133.156C203.949 133.707 202.852 134.508 201.902 135.504C200.953 136.504 200.203 137.656 199.652 139.004C199.102 140.356 198.801 141.805 198.801 143.402C198.801 144.652 199.004 145.75 199.402 146.801C199.754 147.801 200.301 148.652 201 149.352C201.652 150.101 202.5 150.648 203.449 151.051C204.398 151.399 205.449 151.598 206.598 151.598C208 151.598 209.348 151.301 210.598 150.75C211.848 150.199 212.945 149.398 213.895 148.402C214.848 147.449 215.598 146.25 216.145 144.902C216.695 143.551 216.996 142.055 216.996 140.453C216.996 139.203 216.797 138.055 216.395 137.055C215.996 136.055 215.445 135.207 214.797 134.457L214.797 134.457ZM204.301 138.004C204.852 137.305 205.5 136.754 206.25 136.305C207 135.856 207.801 135.656 208.75 135.656C210.199 135.656 211.246 136.106 211.996 137.004C212.746 137.856 213.148 139.106 213.148 140.652C213.148 141.602 212.996 142.555 212.695 143.504C212.445 144.402 212.047 145.203 211.496 145.902C210.949 146.602 210.297 147.152 209.547 147.601C208.797 148 207.949 148.199 207.051 148.199C205.602 148.199 204.551 147.75 203.801 146.902C203.051 146 202.652 144.801 202.652 143.254C202.652 142.305 202.801 141.352 203.102 140.402C203.402 139.504 203.801 138.703 204.301 138.004L204.301 138.004Z" fill-rule="evenodd" />
<path fill="#000000" d="M188.258 132.559L177.961 132.559L174.613 151.301L178.312 151.301L179.559 144.152L186.309 144.152L186.906 140.754L180.16 140.754L181.008 135.957L187.656 135.957L188.258 132.559L188.258 132.559Z" />
<path fill="#98bf00" d="M127.082 44.891C128.43 33.945 125.684 24.102 118.883 15.402C112.086 6.707 103.191 1.66 92.2461 0.309C81.3008 -1.039 71.4531 1.711 62.7578 8.508C54.7109 14.754 49.8125 22.801 48.0625 32.648C47.9141 33.496 47.7617 34.297 47.6641 35.145C47.5625 35.996 47.5117 36.797 47.4648 37.594C47.1133 42.191 47.5625 46.59 48.7617 50.789C50.1133 55.688 52.4609 60.285 55.8594 64.633C59.2578 68.9805 63.1563 72.3828 67.6055 74.9297C71.3516 77.0781 75.5 78.5273 80.0508 79.3281C80.8516 79.4766 81.6484 79.5781 82.5 79.7266C82.9492 79.7773 83.3984 79.8281 83.8477 79.8789C84.9492 75.4297 86.6484 71.2812 88.9961 67.531C87.4453 67.582 85.8477 67.531 84.25 67.383C84.1484 67.332 84.0977 67.332 84.0469 67.332C82.1992 67.082 80.3984 66.734 78.75 66.184C73.6016 64.535 69.2539 61.484 65.707 56.938C62.1562 52.391 60.2578 47.441 59.9062 42.043C59.8086 40.293 59.8594 38.543 60.1094 36.695C60.1094 36.645 60.1094 36.547 60.1094 36.496C61.0586 29.047 64.5078 23 70.4531 18.352C76.4531 13.703 83.1992 11.805 90.7461 12.754C98.293 13.656 104.391 17.102 109.039 23.102C113.688 29.098 115.586 35.844 114.688 43.395C114.438 45.094 114.137 46.691 113.688 48.242C117.887 46.891 122.281 46.191 126.883 46.242C126.93 45.793 127.031 45.344 127.082 44.891L127.082 44.891Z" />
<path fill="#98bf00" d="M132.328 51.488C131.48 51.391 130.68 51.289 129.828 51.238C125.23 50.941 120.832 51.391 116.637 52.539C111.738 53.887 107.141 56.289 102.789 59.688C98.4414 63.035 95.043 66.934 92.5469 71.3828C90.3945 75.1289 88.9453 79.2773 88.0977 83.8281C92.4453 84.5742 96.4453 85.8242 100.141 87.6758C100.391 85.875 100.742 84.1758 101.242 82.5781C102.891 77.4297 105.941 73.082 110.488 69.5312C115.035 65.984 119.984 64.035 125.434 63.684C127.18 63.586 128.93 63.633 130.781 63.883C130.828 63.883 130.879 63.883 130.93 63.883C138.375 64.836 144.426 68.332 149.074 74.2812C153.77 80.2266 155.668 86.9766 154.719 94.5234C153.77 102.07 150.32 108.168 144.375 112.863C138.426 117.512 131.68 119.363 124.23 118.461C125.082 122.512 125.332 126.758 125.031 131.156C134.977 131.809 143.973 128.957 152.02 122.711C160.719 115.914 165.766 107.016 167.113 96.0703C168.465 85.125 165.715 75.2812 158.918 66.582C152.621 58.535 144.574 53.637 134.777 51.891C133.93 51.738 133.129 51.59 132.328 51.488L132.328 51.488Z" />
<path fill="#000000" d="M128.93 78.7266C125.48 78.3281 122.434 79.1797 119.684 81.3281C116.934 83.4766 115.387 86.2266 114.984 89.625C114.535 93.0742 115.387 96.1211 117.535 98.8711C119.684 101.621 122.434 103.168 125.883 103.57C129.281 104.019 132.328 103.168 135.078 101.019C137.828 98.8711 139.375 96.1211 139.824 92.6719C140.227 89.2734 139.375 86.2266 137.227 83.4766C135.078 80.7266 132.328 79.1797 128.93 78.7266L128.93 78.7266Z" />
<path fill="#98bf00" d="M12.8281 73.6289C13.7773 66.082 17.2266 59.938 23.2227 55.289C29.1719 50.641 35.8672 48.742 43.3164 49.691C42.4648 45.641 42.1641 41.395 42.5156 36.996C32.5703 36.344 23.5742 39.145 15.5273 45.441C6.77734 52.238 1.78125 61.137 0.433594 72.082C-0.917969 83.0273 1.78125 92.8242 8.62891 101.57C14.875 109.617 22.9219 114.516 32.7695 116.262C33.5703 116.414 34.3672 116.512 35.2188 116.664C36.0664 116.762 36.8672 116.863 37.7188 116.914C42.3164 117.215 46.7148 116.762 50.9102 115.613C55.7578 114.215 60.4062 111.816 64.7578 108.465C69.0547 105.066 72.4531 101.168 75.0039 96.7695C77.1523 93.0234 78.6016 88.875 79.4492 84.3281C75.1016 83.5781 71.1055 82.2773 67.4062 80.4766C67.1563 82.2266 66.8047 83.9258 66.3047 85.5742C64.6562 90.7227 61.6055 95.0703 57.0586 98.6211C52.5117 102.168 47.5625 104.117 42.1641 104.469C40.4141 104.566 38.6172 104.519 36.7656 104.269C36.7188 104.269 36.668 104.269 36.6172 104.219C29.1719 103.269 23.1211 99.8203 18.4727 93.8711C13.7773 87.875 11.8789 81.1289 12.8281 73.6289L12.8281 73.6289Z" />
<path fill="#000000" d="M32.4688 67.133C29.7188 69.2305 28.1719 72.0312 27.7227 75.4805C27.3203 78.8281 28.1719 81.8789 30.3203 84.625C32.418 87.375 35.168 88.9727 38.6172 89.4258C42.0664 89.7734 45.1133 88.9258 47.8633 86.8242C50.5625 84.6758 52.1094 81.8789 52.5625 78.5273C53.0117 75.0781 52.1602 71.9805 50.0117 69.2812C47.8633 66.535 45.1133 64.984 41.6641 64.586C38.2148 64.133 35.168 64.984 32.4688 67.133L32.4688 67.133Z" />
<path fill="#000000" d="M97.293 32.348C95.1445 29.598 92.3438 28.047 88.9453 27.648C85.4961 27.199 82.4492 28.047 79.75 30.199C77 32.297 75.4023 35.098 75.0039 38.543C74.5508 41.941 75.4531 44.992 77.6016 47.742C79.6992 50.441 82.4492 52.039 85.8984 52.488C89.2969 52.84 92.3438 51.988 95.0938 49.891C97.8438 47.742 99.3906 44.941 99.8438 41.594C100.242 38.145 99.3906 35.047 97.293 32.348L97.293 32.348Z" />
<path fill="#98bf00" d="M85.0469 88.4258C84.5977 88.375 84.1484 88.3242 83.6992 88.2734C82.5977 92.7227 80.8984 96.8711 78.5508 100.621C80.1016 100.519 81.6992 100.57 83.3477 100.769C83.3984 100.769 83.4492 100.769 83.5 100.82C85.3477 101.019 87.0977 101.371 88.7969 101.918C93.9453 103.57 98.293 106.668 101.84 111.215C105.391 115.715 107.289 120.66 107.641 126.109C107.738 127.859 107.688 129.609 107.438 131.457C107.438 131.508 107.438 131.559 107.438 131.656C106.488 139.106 103.039 145.152 97.0938 149.801C91.0938 154.449 84.3477 156.348 76.8008 155.398C69.2539 154.449 63.1563 151 58.5078 145.051C53.8086 139.055 51.9102 132.309 52.8594 124.762C53.0625 123.062 53.4102 121.461 53.9102 119.91C49.6641 121.262 45.2656 121.91 40.6641 121.91C40.6172 122.359 40.5156 122.812 40.4648 123.262C39.1172 134.207 41.8164 144.004 48.6641 152.75C55.4609 161.445 64.3555 166.492 75.3008 167.844C86.2461 169.191 96.043 166.445 104.789 159.645C112.836 153.348 117.734 145.301 119.484 135.457C119.633 134.656 119.734 133.856 119.883 133.008C119.934 132.156 120.035 131.359 120.082 130.559C120.383 125.91 119.934 121.512 118.785 117.363C117.434 112.465 115.035 107.867 111.688 103.519C108.289 99.1719 104.391 95.7227 99.9922 93.2227C96.1953 91.0742 92.0469 89.625 87.4961 88.8242C86.6992 88.6758 85.8984 88.5234 85.0469 88.4258L85.0469 88.4258Z" />
<path fill="#000000" d="M89.9961 120.41C87.8477 117.664 85.0977 116.113 81.6484 115.664C78.1992 115.266 75.1523 116.113 72.4531 118.262C69.7031 120.41 68.1562 123.16 67.7031 126.559C67.2539 130.008 68.1562 133.059 70.3047 135.805C72.4024 138.555 75.1523 140.106 78.6016 140.504C82.0508 140.953 85.0977 140.106 87.8477 137.953C90.5469 135.805 92.0938 133.059 92.5469 129.609C92.9453 126.211 92.0938 123.16 89.9961 120.41L89.9961 120.41Z" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 832 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

Some files were not shown because too many files have changed in this diff Show more