Compare commits

...

268 Commits

Author SHA1 Message Date
Vincent Breitmoser
65a6902c77 test 2025-09-29 22:18:03 +02:00
Vincent Breitmoser
893442bc4e ignore any warnings or lints in dump.rs 2025-09-29 21:45:42 +02:00
Vincent Breitmoser
178dfb9dec db: drop fs database code 2025-09-29 21:44:18 +02:00
Vincent Breitmoser
2e9a14f58e Back out "Fix linting errors after Rust version upgrade."
This backs out commit 0fe99ba962.
2025-09-29 21:17:35 +02:00
Vincent Breitmoser
2395244b8f Back out "Upgrade Rust toolchain: 1.86 -> 1.89"
This backs out commit 3285f19d09.
2025-09-29 21:17:35 +02:00
Vincent Breitmoser
8d21fde2c9 Back out "Upgrade Rust toolchain: 1.89 -> 1.90"
This backs out commit 9e0409bbac.
2025-09-29 21:17:35 +02:00
Zeke Fast
df19ececc3 Fix linting errors after upgrade to 1.90.0.
warning: manual implementation of `.is_multiple_of()`
  --> hagridctl/src/import.rs:95:12
   |
95 |         if (self.count_total % 10) != 0 {
   |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: replace with: `!self.count_total.is_multiple_of(10)`
   |
   = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_multiple_of
   = note: `#[warn(clippy::manual_is_multiple_of)]` on by default

warning: `hagridctl` (bin "hagridctl" test) generated 1 warning (run `cargo clippy --fix --bin "hagridctl" --tests` to apply 1 suggestion)
2025-09-28 10:50:46 +02:00
Zeke Fast
9e0409bbac Upgrade Rust toolchain: 1.89 -> 1.90
If you don't have toolchain installed and you use rustup run:

    $ rustup toolchain install --profile default --component rustfmt,clippy 1.90

NOTE: It might be that you have 1.90.0 installed as stable toolchain, in
that case you still have to run the above command to install exactly 1.90.

Command: `just upgrade-rust`

Changes:
- Upgrade version of used toolchain in the following places:
  - .gitlab-ci.yml
  - Cargo.toml
  - clippy.toml
  - rust-toolchain.toml
2025-09-28 10:50:46 +02:00
Zeke Fast
deefbfabe6 Introduce "upgrade-rust" "just" recipe to automate Rust toolchain upgrade.
Functionality:

User faced functionality is available through `just upgrade-rust`
recipe. It updates Rust versions in the following files through out the
project:
- .gitlab-ci.yml
- Cargo.toml
- clippy.toml
- rust-toolchain.toml

- Checks whether there are any changes of the working copy or in git
  index and refuse to proceed if any asking to commit or stash them.
- Checks current git branch if it does not contains mentions of current stable
  Rust version (with underscores instead of dots) it interactively propose to
  create a new branch.
- Pulls current stable Rust version from
  https://static.rust-lang.org/dist/channel-rust-stable.toml to upgrade
  to it if not specific version was given, i.e. `just upgrade-rust 1.90`
  upgrades version to 1.90 no metter what the current stable Rust
  version is at the moment.
- Upgrades each of the place with where Rust version is used to pulled
  current stable Rust version outputing details about upgrade:
  - .gitlab-ci.yml
  - Cargo.toml
  - clippy.toml
  - rust-toolchain.toml
- Interactively asks whether to commit the changes and if agreed commits them
  to git with detailed message of what was upgraded and to which version.
- Reminds about the need to fix possible compilation and linting errors and
  warnings after upgrade.

Implementation:

Functionality is delivered through main public "upgrade-rust" just
recipe and set of private (starts with underscore) recipes. Private
recipes still can be called from command line
(i.e. for debugging purposes) but they are not listed in the list of
tasks when you type `just` or `just --list`.
For example, you can still call `just _rust-stable-version` to see what
is current stable version of Rust which is retrieved by script.

"upgrade-rust" has dependency (or pre-dependency)
"_ensure-no-vcs-changes" recipe to check for absence of code changes and
post-dependency "_upgrade-rust-fixes-reminder" for the reminder of
compilation and linting fixes.
The main body of "upgrade-rust" recipe extract current versions of Rust
from number of files (called OLD in the code) to be able to output
upgrade messages of what was upgraded.
For this it uses the following private recipes:
- _current-ci-rust-version
- _current-cargo-rust-version
- _current-clippy-rust-version
- _current-toolchain-rust-version
Each of that recipes (with help of variables) encodes specifics of what, how
and from which file should be retrieved.

"_upgrade-rust-git-create-branch" makes check for the current git branch
and interactively propose to create new one switching to it.

The main workflow of version upgrade in different files is in
"_upgrade-rust" private recipe
(yep, there is upgrade-rust user faced recipe and _upgrade-rust a private one).
"_upgrade-rust" is supplied with specific values to upgrade Rust version
e.g. in .gitlab-ci.yml or in rust-toolchain.toml.
So, "upgrade-rust" calls the following tasks
- _upgrade-rust-ci
- _upgrade-rust-cargo
- _upgrade-rust-clippy
- _upgrade-rust-toolchain
to do the upgrade which supply details to "_upgrade-rust" and call it.

After changes of version "upgrade-rust" calls "_upgrade-rust-git-commit"
recipe to commit changes supplying all the details about upgrade, e.g.
old versions, new version and list of files where Rust version was
changed. "_upgrade-rust-git-commit" asks whether you want to commit
changes, if you agree it makes up a commit message and produce commit.

Functionality of editing quite heavily rely on "sed" utility with some
help from tq (tomlq).
For VCS related operations like branch creation, commiting "git" CLI is
used.

Extension:

The implementation design makes it fairly easy to extend recipes to
support new places where Rust has to be upgraded or remove such support
if some files was removed from source tree.

To add support for new place to perform upgrade:
- Add bunch of variables to justfile, e.g.
  CARGO_FILE_NAME := 'Cargo.toml'
  CARGO_FILE_PATH := absolute_path(CARGO_FILE_NAME)
  CARGO_RUST_VERSION_QUERY := 'package.rust-version'

  Addition of CARGO_RUST_VERSION_QUERY depends on format of your file
  and how you will implement your "_current-cargo-rust-version".

  Obviously replace CARGO/cargo in names of variables and recipes with
  your place name.
- Add "_current-cargo-rust-version" recipe to retrieve currently used
  Rust version from your location, rename the function according to the
  new place.
- Add place specific upgrade recipe named properly, .e.g "_upgrade-rust-cargo"
  which calls to "_upgrade-rust" and supply all the required arguments like
  file path, file name, target version, sed command to change the
  version and some parts of the messages to make proper reporting about
  upgrade process.
- Add newly added recipes to the for-loops in "upgrade-rust" recipe, so
  they can be called.
- Add newly added FILE_PATH and FILE_NAME variables to the call of
  "_upgrade-rust-git-commit" in "upgrade-rust" recipe, so new place can
  be listed in git commit and the changes in it can be added to the
  commit.

Changes:
- Extend list of dependencies in README.md with command line tools
  required for "upgrade-rust" recipe:
  - curl
  - sed
  - tq (from `tomlq` crate)
  - git
- Refer to "upgrade-rust" recipe in newly added "Contribution/Housekeeping"
  sesion of README.md.
- Add bunch of variables to justfile related to number of private
  recipes to compound functionality of "upgrade-rust":
  - SED_RUST_VERSION_REGEX
  - RUST_MANIFEST_STABLE_TOML_URL
  - RUST_MANIFEST_STABLE_VERSION_QUERY
  - RUST_MANIFEST_STABLE_VERSION_PARSE_REGEX
  - DEFAULT_RUST_STABLE_VERSION_FORMAT
  - GITLAB_CI_FILE_NAME
  - GITLAB_CI_FILE_PATH
  - CARGO_FILE_NAME
  - CARGO_FILE_PATH
  - CARGO_RUST_VERSION_QUERY
  - CLIPPY_FILE_NAME
  - CLIPPY_FILE_PATH
  - CLIPPY_RUST_VERSION_QUERY
  - TOOLCHAIN_FILE_NAME
  - TOOLCHAIN_FILE_PATH
  - TOOLCHAIN_RUST_VERSION_QUERY
  - GIT_BRANCH_NAME_PREFIX
- Add "upgrade-rust" recipe which upgrade Rust version in the following
  files:
  - .gitlab-ci.yml
  - Cargo.toml
  - clippy.toml
  - rust-toolchain.toml
- Add private just recipes to delivery different aspects of compound
  functionality of "upgrade-rust" recipe:
  - _ensure-no-vcs-changes
  - _upgrade-rust-git-create-branch
  - _upgrade-rust-fixes-reminder
  - _upgrade-rust-git-commit
  - _upgrade-rust
  - _upgrade-rust-ci
  - _upgrade-rust-cargo
  - _upgrade-rust-clippy
  - _upgrade-rust-toolchain
  - _rust-stable-version
  - _current-ci-image
  - _current-ci-rust-version
  - _current-cargo-rust-version
  - _current-clippy-rust-version
  - _current-toolchain-rust-version
2025-09-28 08:06:39 +00:00
Zeke Fast
62a6248b29 Add docs for tests modules: common, common::assert, common::test.
This is to better explain their purpose.
2025-09-28 01:27:27 +00:00
Zeke Fast
3bb755d8d9 Split hagrid::web::tests::maintenance() to individual tests and move them to modules with route declarations.
Changes:
- Split hagrid::web::tests::maintenance() tests to the following:
  - hagrid::routes::vks:tests::get_upload:::maintenance()
  - hagrid::routes::manage::tests::get_manage::maintenance()
  - hagrid::routes::vks:tests::get_verify_token:::maintenance()
  - hagrid::routes::pks::tests::post_pks_add_multipart_form_data::maintenance()
  - hagrid::routes::api::rest::vks::tests::post_vks_v1_upload::maintenance()
  - hagrid::routes::api::rest::vks::tests::post_vks_v1_request_verify_json::maintenance()
  - hagrid::routes::vks::tests::put_root::maintenance()
- Fix tests' endpoints to make them more accurate and actually match
  existing endpoints:
  - GET /verify -> GET /verify/<token>
  - GET /pks/add -> POST /pks/add/ [Content-Type: multipart/form-data]
  - GET /vks/v1/upload -> POST /vks/v1/upload
  - GET /vks/v1/request-verify -> POST /vks/v1/request-verify
- Verify that maintenance state has dissapeared after maintenance file
  removal in each individual test with the same endpoint on which we
  asserted maintenance mesage.
  Previous tests checked only maintenance message disappearance on GET /upload,
  which wasn't completely correct.
- Introduce new fixtures at hagrid::routes::tests::common:
  - token
  - serialized_cert
  - key_multipart_form_data
- Introduce new test module: hagrid::routes::tests::common::test.
  The module is going to accumulate common tests which define common
  workflow for the test and can be called from other tests to check for
  specific behaviour of concreate function or HTTP endpoint.
- Generalize functionality of maintenance test. Move it to module with
  shared tests: hagrid::routes::tests::common::test::maintenance().
2025-09-28 01:27:27 +00:00
Zeke Fast
8e8cb34522 Extract maintenance_text to a fixture to able to reuse it in different tests. 2025-09-28 01:27:27 +00:00
Zeke Fast
9ad13a30a0 Rename check_maintenance() helper to response() to make usage more ergonomic.
I think usage of assert::maintenance::response() looks much better then
assert::maintenance::check_maintenance() or any other variation.
2025-09-28 01:27:27 +00:00
Zeke Fast
63a4445c9f Move hagrid::web::tests::common::assert::check_maintenance() helper to hagrid::routes::tests::common::assert::maintenance::check_maintenance().
Additional changes:
- Adjust imports and usages accordingly.
2025-09-28 01:27:27 +00:00
Zeke Fast
3ce6e8b495 Refactor hagrid::web::tests::maintenance() tests. Remove tests' code duplication.
Changes:
- Extract and generalize assert for absence of the maintenance text from
  hagrid::web::tests::maintenance() test to
  hagrid::routes::tests::common::assert::response_does_not_contain_text()
  assert helper function. Reuse it in the test instead.
- Refactor hagrid::web::tests::common::assert::check_maintenance()
  function to make it more reusable and remove code duplication in its
  implementation:
  - Instead of duplicating response asserting code call for the helper
    hagrid::routes::tests::common::assert::response() and prefill the
    status.
  - Make it accept response instead of passing in client and uri. This
    make check_maintenance() helper more reusable as now we are not
    dependent on what HTTP verb is used to get the response. In
    addition, request occurs directly inside body of the test and helper
    just assert the response. Hence, reparation of responsibilities and
    following SRP.
  - Let check_maintenance() accept the maintenance text which is checked
    in response. The previous implementation of the
    hagrid::web::tests::maintenance() test and the helper was frigile as
    it allows a lot of possibilities for maintenance text to go out of
    coherence.
- Adjust code in hagrid::web::tests::maintenance() test according to
  changed signature of check_maintenance() assertion helper.
- Remove hardcoded checks in hagrid::web::tests::maintenance() test and
  reuse assertion helpers.
2025-09-28 01:27:27 +00:00
Zeke Fast
bf67b3714e Split and move closer to routes declaration tests for hagrid::web::tests::check_response().
Changes:
- Move tests of hagrid::web::tests::check_response() for /search
  endpoint to hagrid::routes::vks::tests::get_search::not_supported_search_query().
  Refactor tests along the way to get rid of boilerplate code.
- Move test of hagrid::web::tests::check_response() for /pks/lookup
  endpoint to hagrid::routes::pks::tests::get_pks_lookup::not_supported_search_query().
  Refactor tests along the way to get rid of boilerplate code.
- Move test of hagrid::web::tests::check_response()  for
  /.well-known/openpgpkey/<domain>/<policy> endpoint to
  hagrid::routes::wkd::tests::get_wkd_policy::wkd_policy_respond_successfully_with_empty_body().
  Refactor tests along the way to get rid of boilerplate code.
2025-09-28 01:27:27 +00:00
Zeke Fast
f54d6ff283 Replace usage of hagrid::web::tests::common::assert::check_response() with hagrid::routes::tests::common::assert::response() to remove code duplication in tests.
Changes:
- Replace usage of hagrid::web::tests::common::assert::check_response()
  with hagrid::routes::tests::common::assert::response() to remove code
  duplication in tests.
- Refactor hagrid::web::tests::check_response() tests to use
  assert::response() helper.
- Take &str instead of &'static str as present_page_text argument in
  assert::response() helper. This was a bug. It affected reusability of
  the assertion helper function.
2025-09-28 01:27:27 +00:00
Zeke Fast
766e97107e Remove pub visibility modifier from "rocket" fixture.
"rocket" fixture ATM used only by "client" fixture. So, it can be
internal to the hagrid::routes::tests::common module. Its export caused
clash of names with "rocket" module from "rocket" crate which forced to
prefix module from crate with ::, i.e. ::rocket.
Using absolute path to the module is probably a good thing by itself
when it used consistently across code base, but in Hagrid's code base it
wasn't and being forced to do that by that tricky coincidence was
annoying and confusing.
2025-09-28 01:27:27 +00:00
Zeke Fast
7f6c4f88aa Move hagrid::web::tests::basic_consistency() test to hagrid::routes::tests module. Move fixtures declarations to hagrid::routes module.
Changes:
- Nest previously incorrectly placed assert module into common module:
  hagrid::routes::tests::assert -> hagrid::routes::tests::common::assert.
- Move hagrid::web::tests::basic_consistency() test to hagrid::routes module.
- Move and rename assertion helper:
  hagrid::web::tests::common::assert::assert_consistency() ->
  hagrid::routes::tests::common::assert::consistency().
- Move fixtures used in routes testing from hagrid::web::tests::common
  module to hagrid::routes::tests::common:
  - base_uri
  - base_uri_onion
  - cert_name
  - alt_cert_name
  - cert
  - configuration
  - rocket
  - client
- Fix imports and usages of moved functions accordingly.
2025-09-28 01:27:27 +00:00
Zeke Fast
6e7fb88000 Introduce tests' response assertion helper: hagrid::routes::tests::assert::response(). Refactor tests to use it.
Changes:
- Introduce tests' response assertion helper:
  hagrid::routes::tests::assert::response().
- Rename hagrid::routes::about::tests::get_about::about_translation() to
  landing_page_is_visible_with_translations().
- Refactor the following tests to use new assertion helper:
  - hagrid::routes::about::tests::get_about::landing_page_is_visible_with_translations()
  - hagrid::routes::about::tests::get_about::privacy_policy_is_visible()
  - hagrid::routes::about::tests::get_about_privacy::privacy_policy_is_visible()
  - hagrid::routes::about::tests::get_about_api::api_docs_are_visible()
  - hagrid::routes::index::tests::get_root::landing_page_is_visible()
  - hagrid::routes::manage::tests::get_manage::delete_form_is_visible()
  - hagrid::routes::vks::tests::get_upload::upload_form_is_visible()
2025-09-28 01:27:27 +00:00
Zeke Fast
1796989bc3 Move case for "GET /manage" (delete_form_is_visible) of hagrid::web::tests::basics() test to hagrid::routes::manage::tests::get_manage module. 2025-09-28 01:27:27 +00:00
Zeke Fast
e389e64c07 Move case for "GET /upload" (upload_form_is_visible) of hagrid::web::tests::basics() test to hagrid::routes::vks::tests::get_upload module. 2025-09-28 01:27:27 +00:00
Zeke Fast
86b89ac7bc Move case for "GET /about/api" (api_docs_are_visible) of hagrid::web::tests::basics() test to hagrid::routes::about::tests::get_about_api module. 2025-09-28 01:27:27 +00:00
Zeke Fast
5720dbe454 Move case for "GET /about/privacy" (privacy_policy_is_visible) of hagrid::web::tests::basics() test to hagrid::routes::about::tests::get_about_privacy module. 2025-09-28 01:27:27 +00:00
Zeke Fast
20ebdbd0e2 Move case for "GET /about" (privacy_policy_is_visible) of hagrid::web::tests::basics() test to hagrid::routes::about::tests::get_about module. 2025-09-28 01:27:27 +00:00
Zeke Fast
090a6f222a Move case for "GET /" (landing_page_is_visible) of hagrid::web::tests::basics() test to hagrid::routes::index::tests::get_root module. 2025-09-28 01:27:27 +00:00
Zeke Fast
74c25c9d9b Move hagrid::web::tests::about_translation() to hagrid::routes::about::tests::get_about module. 2025-09-28 01:27:27 +00:00
Zeke Fast
07804b8833 Refactor hagrid::web::tests::basics() test to use table testing with rstest.
There several caveats to the refactoring:
- Before refactoring all tests ran in strong order as they were
  sequential code in a single function. I believe this was an accident
  behaviour and there were nothing in functionality of basics() fn that
  forced certain sequence of test execution. So, using table testing
  with random order considered by me fine.
- basics() fn tests only read queries. So, it was a discovery for me to
  find at the end of the function check for consistency,
  (i.e. assert_consistency()) as it logically does not make sense.
  I believe that was a test code copy-paste mistake.
  To preserve basic check I moved it to dedicated test basic_consistency().
2025-09-28 01:27:27 +00:00
Zeke Fast
8795469b52 Format code. 2025-09-28 01:27:27 +00:00
Zeke Fast
29ac3534c1 Collapse similar tests into check_response() fn using table testing. 2025-09-28 01:27:27 +00:00
Zeke Fast
b6ad3f3705 Use "base_uri" fixture instead of BASE_URI const. Eliminate usage of BASE_URI const. 2025-09-28 01:27:27 +00:00
Zeke Fast
d9741fad8f Introduce hagrid::web::tests::common::assert module. Move helper assertion fns there.
Changes:
- Move the following functions from hagrid::web::tests module to
  hagrid::web::tests::common::assert:
  - assert_consistency
  - check_maintenance
  - check_null_response
  - check_null_responses_by_email
  - check_responses_by_email
  - check_mr_response
  - check_index_response
  - check_mr_responses_by_fingerprint
  - check_response
  - check_hr_response
  - check_hr_response_onion
  - check_hr_responses_by_fingerprint
  - check_wkd_response
  - check_verify_link
  - check_verify_link_json
  - check_mails_and_verify_email
  - check_mails_and_confirm_deletion
  - pop_mail_capture_pattern
  - vks_publish_submit_multiple
  - vks_publish_submit_get_token
  - vks_publish_submit_response
  - vks_publish_shortcut_get_token
  - vks_publish_json_get_token
  - vks_manage
  - vks_manage_delete
- Change calls to these helper functions around the code accordingly.
2025-09-28 01:27:27 +00:00
Zeke Fast
76ec3eed82 Refactor helper functions in tests of hagrid::web module into fixtures.
Fixtures allow for push-style cascading dependency resolution with
ability to override some of the dependent fixture's arguments when needed.
This is very nice as it gives a lot of flexibility and suitable in a lot
of case. So, single fixture based approach can be applied to most of the
tests.

In addition, this result in good reusability of test code.

If you feel overwelmed and confused with complex argument list of test
functions try to read links from "Documentation" section (see below).

But here is the primer.

    #[fixture]
    pub fn configuration(
        base_uri: &str,
        base_uri_onion: &str,
    ) -> (TempDir, :🚀:figment::Figment) {
        // ...
    }

Attribute #[fixture] turn function into fixture which means now it can
be injected into tests or other fixtures by "configuration" argument name.

"base_uri", "base_uri_onion" are other fixtures which are injected into
"configuration".

    #[fixture]
    pub fn rocket(
        #[from(configuration)] (tmpdir, config): (TempDir, :🚀:figment::Figment),
    ) -> (TempDir, :🚀:Rocket<:🚀:Build>) {
        // ...
    }

As we destructuring results of "configuration" fixture injection into
"rocket" fixture the name of the fixture can't be inferenced from
parameters name, so we have to give a hint with #[from(configuration)]
which fixture has to be called.

    #[rstest]
    fn hkp_add_two(
        base_uri: &str,
        #[from(cert)] (_, tpk_0): (&str, Cert),
        #[with(alt_cert_name())]
        #[from(cert)]
        (_, tpk_1): (&str, Cert),
        #[from(client)] (tmpdir, client): (TempDir, Client),
    ) {
        // ...
    }

This is probably the most trickies function signature, but let brake it
down.

There are 4 fixtures injected into "hkp_add_two" test function:
- base_uri
- cert
- cert
- client

I think at this point of explanation syntax for "base_uri" and "client"
fixturex should be clear.

So,
    #[from(cert)] (_, tpk_0): (&str, Cert),
    #[with(alt_cert_name())] #[from(cert)] (_, tpk_1): (&str, Cert),

calls to "cert" fixture which takes "cert_name" argument. Here is the
definition:

    #[fixture]
    pub fn cert<'a>(cert_name: &'a str) -> (&'a str, Cert) {
        // ...
    }

For the "hkp_add_two" test we need two certificates, so first one is
generated with default name which is returned by "cert_name" fixture,
but for the second with #[with(alt_cert_name())] we say that we want to
use "alt_cert_name" fixture for cert_name argument, so the certificate
is generated with "bar@invalid.example.com" name.

And parenthis for destructing of the results of injection as "cert"
fixture returns tuple of (cert_name, cert).

Documentation:
- https://crates.io/crates/rstest
- https://docs.rs/rstest/0.26.1/rstest/attr.fixture.html

NOTE: probably this changes made translation generation in tests more unstable.
It was already unstable, but it looks like the use of fixtures make a
bit more unstable. So, in case of test failures, just run them several
times. I'll take a look and modules compilation order later once move
tests into modules with SUT's functionality.

Changes:
- Turn the following helper functions from hagrid::web::tests
  module to fixtures:
  - configuration()
  - rocket()
  - client()
- Panic immediately in the fixtures and don't bubble up the error like
  it was in helper functions. Bubbling up errors does not make sense as
  we panic with expect later in tests code. So, we duplicated code
  (.unwrap()) calls) and make return signatures more complex.
- Inject "base_uri" and "base_uri_onion" into "configuration" fixture as
  after turning it into fixture we can do that.
- Remove BASE_URI_ONION constant as it is not used. Now "base_uri_onion"
  fixture is used instead.
- Refactor all the tests which used mentioned above helper functions to
  use fixture.
2025-09-28 01:27:27 +00:00
Zeke Fast
0d868ce27e Replace "build_cert" helper fn in tests of hagrid::web module with "cert" fixture.
Changes:
- Turn "build_cert" funtion into fixture.
- Refactor all tests which relied on build_cert() to inject one or two
  certs.
- Replace usage of duplicated e-mail (cert_name) in tests with value
  returned by "cert" fixture (i.e. cert_name). This better align actual
  cert and cert_name usage in tests and eliminate hardcoded e-mail
  string slices.
2025-09-28 01:27:27 +00:00
Zeke Fast
d32b48885e Declare tests in hagrid::web module with #[rstest] instea of #[test].
This should allow to use all the rstest goodies with these tests.
2025-09-28 01:27:27 +00:00
Zeke Fast
b11f7dc7b3 Extract BASE_URI and BASE_URI_ONION to rstest::fixture's. Use fixture injection in tests.
Changes:
- Create fixtures based on constants:
  - base_uri
  - base_uri_onion
- Change tests which use BASE_URI or BASE_URI_ONION from #[test]
  declaration to #[rstest]. That allows to inject fixtures.
- Replace usage of constants BASE_URI and BASE_URI_ONION with base_uri
  and base_uri_onion fixtures.
  There are still some usages of the constants which will be refactored
  later.
- Propagate injected fixture values into check_* assertions to replace
  constant usages.
2025-09-28 01:27:27 +00:00
Zeke Fast
f8c4871b61 Add "rstest" as dev-dependency to hagrid's Cargo.toml.
Additional changes:
- Update Cargo.lock: `just build`
2025-09-28 01:27:27 +00:00
Zeke Fast
93aa79e979 Remove outdated comment in clippy.toml.
Hagrid has switched to 1.89 and the comment does not make any sense
anymore.
2025-09-28 01:18:44 +00:00
Zeke Fast
0fe99ba962 Fix linting errors after Rust version upgrade.
Command: just lint

Changes:
- Fix the following linting errors:

    warning: this `if` statement can be collapsed
       --> database/src/fs.rs:335:5
        |
    335 | /     if let Ok(target) = read_link(link) {
    336 | |         if target == expected {
    337 | |             remove_file(link)?;
    338 | |         }
    339 | |     }
        | |_____^
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#collapsible_if
        = note: `#[warn(clippy::collapsible_if)]` on by default
    help: collapse nested if block
        |
    335 ~     if let Ok(target) = read_link(link)
    336 ~         && target == expected {
    337 |             remove_file(link)?;
    338 ~         }
        |

    warning: this `if` statement can be collapsed
       --> database/src/fs.rs:510:9
        |
    510 | /         if let Ok(target) = read_link(&link_fpr) {
    511 | |             if target == expected {
    512 | |                 remove_file(&link_fpr)?;
    513 | |             }
    514 | |         }
        | |_________^
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#collapsible_if
    help: collapse nested if block
        |
    510 ~         if let Ok(target) = read_link(&link_fpr)
    511 ~             && target == expected {
    512 |                 remove_file(&link_fpr)?;
    513 ~             }
        |

    warning: this `if` statement can be collapsed
       --> database/src/fs.rs:515:9
        |
    515 | /         if let Ok(target) = read_link(&link_keyid) {
    516 | |             if target == expected {
    517 | |                 remove_file(link_keyid)?;
    518 | |             }
    519 | |         }
        | |_________^
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#collapsible_if
    help: collapse nested if block
        |
    515 ~         if let Ok(target) = read_link(&link_keyid)
    516 ~             && target == expected {
    517 |                 remove_file(link_keyid)?;
    518 ~             }
        |

    warning: this `if` statement can be collapsed
       --> database/src/fs.rs:630:9
        |
    630 | /         if let Ok(link_fpr_target) = link_fpr.canonicalize() {
    631 | |             if !link_fpr_target.ends_with(&path_published) {
    632 | |                 info!(
    633 | |                     "Fingerprint points to different key for {} (expected {:?} to be suffix of {:?})",
    ...   |
    638 | |         }
        | |_________^
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#collapsible_if
    help: collapse nested if block
        |
    630 ~         if let Ok(link_fpr_target) = link_fpr.canonicalize()
    631 ~             && !link_fpr_target.ends_with(&path_published) {
    632 |                 info!(
    ...
    636 |                 return Err(anyhow!(format!("Fingerprint collision for key {}", fpr)));
    637 ~             }
        |

    warning: this `if` statement can be collapsed
       --> database/src/fs.rs:640:9
        |
    640 | /         if let Ok(link_keyid_target) = link_keyid.canonicalize() {
    641 | |             if !link_keyid_target.ends_with(&path_published) {
    642 | |                 info!(
    643 | |                     "KeyID points to different key for {} (expected {:?} to be suffix of {:?})",
    ...   |
    648 | |         }
        | |_________^
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#collapsible_if
    help: collapse nested if block
        |
    640 ~         if let Ok(link_keyid_target) = link_keyid.canonicalize()
    641 ~             && !link_keyid_target.ends_with(&path_published) {
    642 |                 info!(
    ...
    646 |                 return Err(anyhow!(format!("KeyID collision for key {}", fpr)));
    647 ~             }
        |

    warning: this `if` statement can be collapsed
       --> database/src/lib.rs:534:9
        |
    534 | /         if let Some(current_fpr) = current_link_fpr {
    535 | |             if current_fpr != *fpr_primary {
    536 | |                 self.set_email_unpublished_filter(tx, &current_fpr, |uid| {
    537 | |                     Email::try_from(uid)
    ...   |
    542 | |         }
        | |_________^
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#collapsible_if
    help: collapse nested if block
        |
    534 ~         if let Some(current_fpr) = current_link_fpr
    535 ~             && current_fpr != *fpr_primary {
    536 |                 self.set_email_unpublished_filter(tx, &current_fpr, |uid| {
    ...
    540 |                 })?;
    541 ~             }
        |

    warning: this `if` statement can be collapsed
      --> hagridctl/src/delete.rs:79:9
       |
    79 | /         if err.is_ok() {
    80 | |             if let Err(e) = result {
    81 | |                 err = Err(e);
    82 | |             }
    83 | |         }
       | |_________^
       |
       = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#collapsible_if
       = note: `#[warn(clippy::collapsible_if)]` on by default
    help: collapse nested if block
       |
    79 ~         if err.is_ok()
    80 ~             && let Err(e) = result {
    81 |                 err = Err(e);
    82 ~             }
       |

    warning: this `if` statement can be collapsed
       --> hagridctl/src/import.rs:197:9
        |
    197 | /         if !acc.is_empty() {
    198 | |             if let Packet::PublicKey(_) | Packet::SecretKey(_) = packet {
    199 | |                 callback(acc);
    200 | |                 acc = vec![];
    201 | |             }
    202 | |         }
        | |_________^
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#collapsible_if
    help: collapse nested if block
        |
    197 ~         if !acc.is_empty()
    198 ~             && let Packet::PublicKey(_) | Packet::SecretKey(_) = packet {
    199 |                 callback(acc);
    200 |                 acc = vec![];
    201 ~             }
        |

    warning: this `if` statement can be collapsed
       --> src/dump.rs:522:25
        |
    522 | /                         if pd.mpis {
    523 | |                             if let Ok(ciphertext) = e.ciphertext() {
    524 | |                                 pd.dump_mpis(output, &ii, &[ciphertext], &["ciphertext"])?;
    525 | |                             }
    526 | |                         }
        | |_________________________^
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#collapsible_if
        = note: `#[warn(clippy::collapsible_if)]` on by default
    help: collapse nested if block
        |
    522 ~                         if pd.mpis
    523 ~                             && let Ok(ciphertext) = e.ciphertext() {
    524 |                                 pd.dump_mpis(output, &ii, &[ciphertext], &["ciphertext"])?;
    525 ~                             }
        |
2025-09-28 01:18:44 +00:00
Zeke Fast
0dceaa454f Fix compilation warnings after Rust version upgrade.
Command: `just check`

Changes:
- The following warnings were fixed:

    warning: hiding a lifetime that's elided elsewhere is confusing
       --> database/src/sqlite.rs:146:11
        |
    146 |     fn tx(&self) -> &Transaction {
        |           ^^^^^     ------------
        |           |         ||
        |           |         |the same lifetime is hidden here
        |           |         the same lifetime is elided here
        |           the lifetime is elided here
        |
        = help: the same lifetime is referred to in inconsistent ways, making the signature confusing
        = note: `#[warn(mismatched_lifetime_syntaxes)]` on by default
    help: use `'_` for type paths
        |
    146 |     fn tx(&self) -> &Transaction<'_> {
        |                                 ++++

    warning: hiding a lifetime that's elided elsewhere is confusing
      --> src/dump.rs:22:30
       |
    22 |     pub fn display_sensitive(&self) -> SessionKeyDisplay {
       |                              ^^^^^     ----------------- the same lifetime is hidden here
       |                              |
       |                              the lifetime is elided here
       |
       = help: the same lifetime is referred to in inconsistent ways, making the signature confusing
       = note: `#[warn(mismatched_lifetime_syntaxes)]` on by default
    help: use `'_` for type paths
       |
    22 |     pub fn display_sensitive(&self) -> SessionKeyDisplay<'_> {
       |                                                         ++++
2025-09-28 01:18:44 +00:00
Zeke Fast
6060fcf0bc Upgrade Debian image version in CI: bookworm -> trixie.
Debian 13.0 (codename: trixie) was released on 2025-08-09.

Changes:
- Update version of the used CI image: 1.89-bookworm -> 1.89-trixie
2025-09-28 01:18:44 +00:00
Zeke Fast
3285f19d09 Upgrade Rust toolchain: 1.86 -> 1.89
If you don't have toolchain installed and you use rustup run:

    $ rustup install --component rustfmt --component clippy 1.89

NOTE: It might be that you have 1.89.0 installed as stable toolchain, in
that case you still have to run the above command to install exactly
1.89.

Changes:
- Upgrade version of used toolchain in the following places:
  - .gitlab-ci.yml
  - Cargo.toml
  - clippy.toml
  - rust-toolchain.toml
2025-09-28 01:18:44 +00:00
Zeke Fast
82f48d3a4e Code review issue: Add a reminder comment to hook up initializers for execution. 2025-09-27 22:54:01 +02:00
Zeke Fast
5f819e8004 Code review issue: Reorder handlers in hagrid::routes::api::rest::vks module.
This it to make order to more organized and logical.
2025-09-27 22:42:30 +02:00
Zeke Fast
d7b5796abf Fix translation generation. 2025-08-23 12:49:59 +02:00
Zeke Fast
5beafb2881 Format code. 2025-08-23 07:42:20 +02:00
Zeke Fast
d8fcaa7d5e Introduce idea of initalizers. Extract rocket bootstrap logic from hagrid::web and rocket_factory() function to initializers.
Continue to dissolve hagrid::web module which had way too much responsibilities
and became the god-module (see https://en.wikipedia.org/wiki/God_object).

In addition, solved and refined several smaller issues and made
improvements.

Changes:
- Extract configuration loading (i.e. extraction) from
  hagrid::web::rocket_factory() to hagrid::config::load() and call it
  from hagrid::app::configure_rocket().
- Extract and split hagrid::web::run() (which previously was #[launch] on
  rocket() fn) to hagrid::app::run(), hagrid::app::configure_rocket()
  and hagrid::app::run_rocket().
- Remove String copy in Configuration::base_uri_onion() and return
  references to internal fields of the struct. Callers can copy it when
  they need to, otherwise they can just use references without overhead.
- Extract rocket() helper function from client() in tests of hagrid::web
  module to separate rocket instance creation.
- Remove code duplication in tests of hagrid::web module by reusing
  rocket() and client() functions for instances construction.
- Introduce idea of initializers and "hagrid::initializers" module to bootstrap
  rocket framework and register states (with rocket.manage()), fairings (with
  rocket.attach()) and routes (with rocket.mount()) on rocket instance.

  A single initializer resides in submodule of hagrid::initializers and
  register itself in hagrid::initializers::run() function (has to be
  done manually by the programmer who adds new initalizer).
  Initializer consist of the following two functions with some vary signatures:
  - init(config: &Configuration) -> hagrid::initalizers::Return<CONSTRUCTED_TYPE>
  - register(rocket: Rocket<Build>, config: &Configuration, state_fairing_or_routes: <CONSTRUCTED_TYPE>) -> Rocket<Build>

  init(config: &Configuration)
  The function instantiate state, fairing or routes possibly using
  config for it.
  Tricky moment about it is its return type which has to be
  hagrid::initializers::Result. This return type is an alias to
  anyhow::Result<Option<T>>. When construction fails for whatever reason it
  returns Err. If everything went well, but construction is not a desired
  behaviour, i.e. because of it was disabled in configuration, like we
  do for "prometheus" initializer init() should return Ok(None).
  Errors then handled in hagrid::initializers::run() function (by
  initialize!() macro to be precise) and register the instance if it was
  created.

  register(rocket: Rocket<Build>, config: &Configuration, state_fairing_or_routes: <CONSTRUCTED_TYPE>)
  Receives unwrapped instance of state, fairing or routes if it was
  constructed and register it on rocket.
  Some instances can be registered several times, e.g. like as fairing
  and as routes. We do that for PrometheusMetrics.
- Move hagrid::web::ApplicationState (what was HagridState before
  renaming) to hagrid::app::state::ApplicationState.
- Dissolve hagrid::web::rocket_factory() and set of configure_*
  functions into initializers preserving the order of their registration
  on rocket, though it might be not so important.
- Replace usage of hagrid::web::rocket_factory() in hagrid::web
  module's tests with hagrid::app::configure_rocket() which become
  approximate functional equivalent of it but calls for initializers to
  populate the rocket.
2025-08-23 06:39:27 +02:00
Zeke Fast
fb0d0c24c4 Rename hagrid::web::HagridState to ApplicationState. 2025-08-21 17:55:41 +02:00
Zeke Fast
34d056ea55 Extract route handlers from hagrid::web to newly added hagrid::routes module.
hagrid::web module was and is highly overloaded with functionality and
contains all kind of code from business logic to http handling.

With this commit I tried to offload complexity of handler to a dedicated
module to make reasoning about available endpoints a bit easier.

Changes:
- Move the following endoints to hangrid::routes module and its newly
  added submodules:
  - hagrid::web::routes()                               -> hagrid::routes::routes()
  - hagrid::web::root()                                 -> hagrid::routes::index::root()
  - hagrid::web::about()                                -> hagrid::routes::about::about()
  - hagrid::web::news()                                 -> hagrid::routes::about::news()
  - hagrid::web::news_atom()                            -> hagrid::routes::atom::news_atom()
  - hagrid::web::privacy()                              -> hagrid::routes::about::privacy()
  - hagrid::web::apidoc()                               -> hagrid::routes::about::apidoc()
  - hagrid::web::faq()                                  -> hagrid::routes::about::faq()
  - hagrid::web::usage()                                -> hagrid::routes::about::usage()
  - hagrid::web::files()                                -> hagrid::routes::assets::files()
  - hagrid::web::stats()                                -> hagrid::routes::about::stats()
  - hagrid::web::errors()                               -> hagrid::routes::errors::errors()
  - hagrid::web::vks_api::vks_v1_by_email()             -> hagrid::routes::api::rest::vks::vks_v1_by_email()
  - hagrid::web::vks_api::vks_v1_by_fingerprint()       -> hagrid::routes::api::rest::vks::vks_v1_by_fingerprint()
  - hagrid::web::vks_api::vks_v1_by_keyid()             -> hagrid::routes::api::rest::vks::vks_v1_by_keyid()
  - hagrid::web::vks_api::upload_json()                 -> hagrid::routes::api::rest::vks::upload_json()
  - hagrid::web::vks_api::upload_fallback()             -> hagrid::routes::api::rest::vks::upload_fallback()
  - hagrid::web::vks_api::request_verify_json()         -> hagrid::routes::api::rest::vks::request_verify_json()
  - hagrid::web::vks_api::request_verify_fallback()     -> hagrid::routes::api::rest::vks::request_verify_fallback()
  - hagrid::web::vks_web::search()                      -> hagrid::routes::vks::search()
  - hagrid::web::vks_web::upload()                      -> hagrid::routes::vks::upload()
  - hagrid::web::vks_web::upload_post_form()            -> hagrid::routes::vks::upload_post_form()
  - hagrid::web::vks_web::upload_post_form_data()       -> hagrid::routes::vks::upload_post_form_data()
  - hagrid::web::vks_web::request_verify_form()         -> hagrid::routes::vks::request_verify_form()
  - hagrid::web::vks_web::request_verify_form_data()    -> hagrid::routes::vks::request_verify_form_data()
  - hagrid::web::vks_web::verify_confirm()              -> hagrid::routes::vks::verify_confirm()
  - hagrid::web::vks_web::verify_confirm_form()         -> hagrid::routes::vks::verify_confirm_form()
  - hagrid::web::vks_web::quick_upload()                -> hagrid::routes::vks::quick_upload()
  - hagrid::web::vks_web::quick_upload_proceed()        -> hagrid::routes::vks::quick_upload_proceed()
  - hagrid::web::debug_web::debug_info()                -> hagrid::routes::debug::debug_info()
  - hagrid::web::hkp::pks_lookup()                      -> hagrid::routes::pks::pks_lookup()
  - hagrid::web::hkp::pks_add_form()                    -> hagrid::routes::pks::pks_add_form()
  - hagrid::web::hkp::pks_add_form_data()               -> hagrid::routes::pks::pks_add_form_data()
  - hagrid::web::hkp::pks_internal_index()              -> hagrid::routes::pks::pks_internal_index()
  - hagrid::web::wkd::wkd_policy()                      -> hagrid::routes::wkd::wkd_policy()
  - hagrid::web::wkd::wkd_query()                       -> hagrid::routes::wkd::wkd_query()
  - hagrid::web::manage::vks_manage()                   -> hagrid::routes::manage::vks_manage()
  - hagrid::web::manage::vks_manage_key()               -> hagrid::routes::manage::vks_manage_key()
  - hagrid::web::manage::vks_manage_post()              -> hagrid::routes::manage::vks_manage_post()
  - hagrid::web::manage::vks_manage_unpublish()         -> hagrid::routes::manage::vks_manage_unpublish()
  - hagrid::web::maintenance::maintenance_error_web()   -> hagrid::routes::maintenance::maintenance_error_web()
  - hagrid::web::maintenance::maintenance_error_json()  -> hagrid::routes::maintenance::maintenance_error_json()
  - hagrid::web::maintenance::maintenance_error_plain() -> hagrid::routes::maintenance::maintenance_error_plain()
2025-08-21 16:01:18 +02:00
Zeke Fast
eb4ffd59f4 Collect adhoc configuration extraction into hagrid::app::config module. Introduce Configuration struct.
Adhoc configuration value extraction from Figment is hard to reason on
application level, so it is better to create application wide
configuration. Apart of that having a unified module, like
hagrid::app::config, should allow to consolidate config logic in a
single place and simplify many other places.

Changes:
- Introduce hagrid::app::config module and Configuration struct in it.
- Migrate and consolidate adhoc config value extraction to a single
  struct hagrid::app::config::Configuration.
- Replace logic to calculate default values in configuration in cases
  when values were not provided with functionality from "serde" crate,
  i.e. using #[serde(default)] or #[serde(default = ...)] to fill
  missing values with defaults.
- Clean up signatures of configure_* like functions in hagrid::web::
  module as some of them after moving configuration extraction can not
  fail any more.
- Replace configuration propagation from pull to push model in
  configure_* functions. Before the refactoring they all depend on
  figment and configuration has been pulled from figment and put into
  modules, now we explicitly push it. That's reduce dependency on global
  configuration. That still might be reworked again once we move module
  creation to hagrid::app module from hagrid::web.
2025-08-21 12:36:23 +02:00
Zeke Fast
5ed05975e7 Extract routes from local variable in rocket_factory() function to routes().
Routes are supposed to change from time to time extraction to a
dedicated function isolates them from rocket setup code in
rocket_factory() function.
2025-08-20 23:10:23 +02:00
Zeke Fast
9b6b495f56 Extract modules declaration from main.rs to lib.rs in "hagrid" crate.
This is to make "hagrid" code library alike instead of having it as a binary
which improves reusability and testability of the code.

So, the main.rs binary will be just a launcher for library code.

Changes:
- Extract modules declaration from main.rs to lib.rs in "hagrid" crate.
- Introduce "app" module which has to accumulate infrustructure code,
  but for the time being it delegates run() and handle errors.
- Rename hagrid::web::serve() to hagrid::web::run() to standardize on
  module entry function names.
- Accumulate rocket framework launching code in hagrid::web::run()
  function.
  To be able to move code around I have to replace usage of
  macros like #[rocket::launch] or #[rocket::main] with direct usage of
  :🚀:async_main, ignite() and launch() functions.
2025-08-20 22:24:40 +02:00
Zeke Fast
9adeb4d544 Clean ups in justfile. 2025-08-20 21:17:36 +02:00
Zeke Fast
015e698725 Clean up unused imports warnings which appeared in tests.
Cleaned warnings:

    warning: unused import: `std::time::SystemTime`
      --> database/src/test.rs:19:5
       |
    19 | use std::time::SystemTime;
       |     ^^^^^^^^^^^^^^^^^^^^^
       |
       = note: `#[warn(unused_imports)]` on by default

    warning: unused import: `PublicParts`
      --> database/src/test.rs:24:55
       |
    24 | use sequoia_openpgp::packet:🔑:{Key4, PrimaryRole, PublicParts, SecretParts};
       |                                                       ^^^^^^^^^^^

    warning: unused imports: `Features` and `HashAlgorithm`
      --> database/src/test.rs:25:30
       |
    25 | use sequoia_openpgp::types::{Features, HashAlgorithm};
       |                              ^^^^^^^^  ^^^^^^^^^^^^^
2025-08-20 02:09:37 +02:00
Vincent Breitmoser
8b89ab112a version 2.1.0 2025-08-01 10:05:50 +02:00
Vincent Breitmoser
7d3194dd25 cargo: cargo update 2025-08-01 10:01:20 +02:00
Wiktor Kwapisiewicz
5aa404fc32 Split tests for: naked key, DKS key, revoked key 2025-07-31 11:51:27 +02:00
Wiktor Kwapisiewicz
5b28cedf37 Fix naked-key upload test (test_no_selfsig)
Previously, the test used Sequoia's high-level API which attached a
Direct-Key Signature to the key. As such, this wasn't a truly
naked-key.

This test uses low-level API to contruct a bare key and checks if the
import fails. Then it attaches a DKS which causes the import to
succeed. Then it revokes that key and checks if the revocation is
correctly propagated.
2025-07-28 13:28:04 +02:00
Wiktor Kwapisiewicz
12f0eef5be Consider keys OK if they have at least one self-signature
This is especially useful for small keys with only a Direct Key Signature.

Closes: https://gitlab.com/keys.openpgp.org/hagrid/-/issues/180
2025-07-24 10:50:01 +02:00
Daniel Huigens
94bf37a6a3 Add logo 2025-07-10 16:26:53 +02:00
Vincent Breitmoser
ce8a6deed0 nix: add hagridctl package 2025-06-17 10:17:01 +02:00
Vincent Breitmoser
df221eaf2b nix: update and fix nix files for new build 2025-06-17 10:09:35 +02:00
Zeke Fast
7532ff4b22 Fix compilation errors after crates upgrade. 2025-05-12 08:15:44 +00:00
Zeke Fast
ca7d0406cf Upgrade rocket and related crates.
Changes:
- Upgrade the following crate dependencies in Cargo.toml:
  - log: 0.4.22 -> 0.4.27
  - rocket_dyn_templates: 0.1.0 -> 0.2.0
  - rocket_prometheus: 0.10.0 -> 0.10.1
  - handlebars: 4.5.0 -> 5.1.2 (indirect dependency of rocket_dyn_templates)
- Update Cargo.lock.
2025-05-12 08:15:44 +00:00
Zeke Fast
784d866da0 Remove aliasing of Sqlite as KeyDatabase in database/src/lib.rs. Change usages accordingly.
I believe KeyDatabase name is the thing of the past which was made to
simplify migration to Sqlite without doing the renaming all over code
base (which this commit does). So, this is a bit of technical debt IMO
and direct usage of Sqlite would serve better code readability.
KeyDatabase leaks presence of Sqlite though functions like new_file() and
others as such it doesn't serve very good the purpose of abstracting
things away and hiding the fact of Sqlite behind.
2025-05-12 07:53:18 +02:00
Zeke Fast
6a4e20a41f Remove hagridctl/Rocket.toml config as we don't use it in hagridctl any more. 2025-05-06 17:19:31 +02:00
Zeke Fast
e7b7b994ce Fix punctuation in help messages. 2025-05-06 10:31:32 +02:00
Zeke Fast
4a6ba18b33 Remove unnecessary Result:: type specification and use Ok() directly in database::Sqlite.
Awhile ago we returned default Result type to core::result::Result
instead of anyhow::Result. So, such prefixing is not needed any more.
2025-05-06 10:31:28 +02:00
Zeke Fast
d9afbd151c Replace "-c, --config" and "-e, --environment" opts with "--db-file-path" and "HAGRID_DB_FILE_PATH" env var.
Code review issue: https://gitlab.com/keys.openpgp.org/hagrid/-/merge_requests/214#note_2484117659

Changes:
- Extract database file path and log dir path constructions from
  Sqlite::new_file() and Sqlite::new_internal() associated functions to
  Sqlite::db_file_path() and Sqlite::log_dir_path().
  These path manipulations inside Sqlite/KeyDatabase wasn't ideal as
  they increased diversity of reasons why Sqlite constructions can fail,
  unnecessary constrain how Sqlite and with which paths can be used, and
  that logic doesn't actually belong to Sqlite itself. So, later we
  probably have to move these db_file_path() and log_dir_path()
  functions else where.
- Replace base_dir parameter of Sqlite::new_file() with "db_file" and "log_dir"
  params which gives us a way to use database file passed in command
  line options.
- Add Sqlite::log_dir_path_from_db_file_path() associated function to
  infer log_dir_path based on given db_file_path.
  It is used in command handler where only db_file_path provided.
- Pull up log dir construction from Sqlite::new_internal() to
  Sqlite::new_file() to avoid passing through log_dir_path through
  several functions. This also makes Sqlite::new_file() signature
  cleaner.
- Fix usages of Sqlite/KeyDatabase::new_file through out the code base:
  and instead of providing base_dir, populate db_file_path and
  log_dir_path using Sqlite::db_file_path() and Sqlite::log_dir_path()
  functions which calculate paths based on provided base_dir.
- Remove "-c, --config" and "-e, --environment" options from
  hagridctl::cli::Cli.
- Remove hagridctl::cli::DeploymentEnvironment enum.
- Remove entire hagridctl::config module as we don't extract paths from
  configuration any more.
- Add "env" feature flag to "clap" dependency in hagridctl/Cargo.toml to
  be able to populate Cli::db_file_path field from HAGRID_DB_FILE_PATH
  environment variable.
- Add optional --db-file-path option to "hagridctl". Cli::db_file_path
  is obligatory, but when --db-file-path is not provided Clap tries to
  populate it from HAGRID_DB_FILE_PATH env var and if that's not
  possible Clap terminates with error message.
- In hagridctl::cli::dispatch_cmd() use db_file_path from
  cli.db_file_path instead of keys_internal_dir extracted from configuration.
- Replace keys_db_internal_dir with db_file_path in the following
  command handlers:
  - hagridctl::delete::run
  - hagridctl::import::run
- Get rid of unnecessary Result unwrapping and wrapping it again in
  hagrid::web::configure_db_service function and return the value from
  KeyDatabase::new_file immediately instead.
2025-05-06 10:30:18 +02:00
Zeke Fast
d7de01d023 Add CLI's tests using clap::Command::debug_assert().
Documentation: https://docs.rs/clap/latest/clap/struct.Command.html#method.debug_assert
2025-05-05 01:28:59 +02:00
Zeke Fast
eb36332f8b Validate presence of help text for opts and args of hagridctl and tester bins.
Changes:
- Set "help_expected = true" for the following CLI structs:
  - hagridctl::cli::Cli
  - tester::cli::Cli
- Fill missing help texts.
2025-05-05 01:26:23 +02:00
Zeke Fast
e2594b019c Add "--workspace" flag to "just build" recipe, so it builds all Rust code.
Additional changes:
- In case of necessity specific binary can be provided for "just build"
  recipe, e.g. "just build -p hagrid" or "just build -p hagridctl".
2025-05-05 00:05:23 +02:00
Zeke Fast
81b333bd43 Replace "base" arg in "hagridctl delete" cmd with keys_internal_dir extracted from config.
Code review issue: https://gitlab.com/keys.openpgp.org/hagrid/-/merge_requests/214#note_2482983036

Changes:
- Change type of "base_dir" argument of database::Sqlite::new_file()
  associated function form "impl Into<PathBuf>" to "impl AsRef<Path>"
  as AsRef<Path> is more generic.
- Remove "base" argument from "hagridctl delete" command.
- Extract "config" and "environment" options from "import" command to
  hagridctl::cli::Cli struct.
- Make "config" and "environment" options global, i.e. they can be
  specified with any command.
- Change "environment" from being argument to option as being argument
  doesn't play well with "import" command where arbitrary list of
  KEYRING_FILES can be specified. In general, being global works much
  better with options then arguments.
  So, <ENVIRONMENT> argument became "-e, --environment <ENVIRONMENT>" option
  with the same default value (production).
- KEYRING_FILES in "import" command don't need to be "last" any more.
- Get "keys_db_internal_dir" in hagridctl::cli::dispatch_cmd() and let
  it be propagated to places where KeyDatabase is created instead of
  progapagation of config or base_dir.
2025-05-04 23:37:54 +02:00
Zeke Fast
8fad06e11d Move args related to "import" cmd from top level to "import" command. Clean ups.
Before merging "hagrid-delete" into "hagridctl" the later was
exclusively used with "import" command. So, it doesn't make a difference
how "config" and "ENVIRONMENT" parameters were specified. After merge
these arguments doesn't make sense at the top level any more, hence the
move.

Changes:
- Move "file" and "env" field from hagridctl::cli::Cli struct into
  hagridctl::cli::Command::Import enum variant.
- Because of the move and optionality of "ENVIRONMENT" argument for
  "import" command I had to add "last = true" option to KEYRING_FILES
  argument which requires their specification to be prepended with --.
- Clean up hagridctl::cli::dispatch_cmd() function as match statement
  now can take ownership over cli.command and "file" and "env" fields of
  Command::Import can be destructured and passed directly to
  config::load() instead of passing around reference to Cli struct.
  Overall code becomes cleaner, IMO.
- Change config::load() to take "file" and "env" directly instead of
  "cli" parameter.
- Get rid of run() function in hagridctl/src/main.rs as it becomes
  redundant.
2025-05-04 20:47:53 +02:00
Zeke Fast
9c4b51fa61 Merge "hagrid-delete" binary into "hagridctl" crate.
Code review issue: https://gitlab.com/keys.openpgp.org/hagrid/-/merge_requests/214#note_2482960066

Changes:
- Remove "hagrid-delete" bin declaration from Cargo.toml.
- Remove "clap" dependency for "hagrid" crate in Cargo.toml as after
  moving "hagrid-delete" to "hagridctl" "hagrid" crate does not use
  "clap" any more.
- Remove "run-hagrid-delete" recipe with its "hagrid-delete" and
  "delete" aliases from justfile.
- Update Cargo.lock.
- Create "delete" command for "hagridctl" by adding cli::Command::Delete
  variant which previously were hagrid-delete::cli::Cli struct.
- Move "delete" module with command handler ("run" function) from
  "hagrid" crate to hagridctl::delete.
- Extend hagridctl::cli::dispatch_cmd() function with processing of
  cli::Command::Delete variant and call to hagridctl::delete::run.
- Move print_errors() from hagrid::delete::cli module to hagrdictl::cli.
- Move KeyDatabase instantiation from hagrid/src/main.rs into
  hagridctl::delete::run command handler as this way of database
  instantiation is specific to "delete" command.
  Probably later we have to reconsider how we instantiate database for
  all the commands to avoid reimplementing that functionality every time
  and duplicating the code.
  That move caused change in signature of hagridctl::delete::run
  function. Now we pass base path instead of db reference.
2025-05-04 20:19:02 +02:00
Zeke Fast
9f5d8b3706 Change text in "tester" crate "description" field.
Code review issue: https://gitlab.com/keys.openpgp.org/hagrid/-/merge_requests/214#note_2482945343
2025-05-04 18:04:59 +02:00
Zeke Fast
24034536e8 Clean up linting warnings.
Commands:

    just lint

Warnings:

    warning: this expression creates a reference which is immediately dereferenced by the compiler
      --> hagridctl/src/cli.rs:39:58
       |
    39 |         Command::Import { keyring_files } => import::run(&config, keyring_files.to_owned()),
       |                                                          ^^^^^^^ help: change this to: `config`
       |
       = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_borrow
       = note: `#[warn(clippy::needless_borrow)]` on by default
2025-05-04 11:02:02 +02:00
Zeke Fast
c901336b12 Format code. 2025-05-04 11:02:02 +02:00
Zeke Fast
1442bed329 Rename command handler from module_name::do_something pattern to module_name::run.
I think it looks a bit weird to have things like module "import" with function
"do_import" which is a command handler. So, I renamed all such command
handlers to simple "run" functions. IMO on call side it looks now
better, e.g.: import::run().

Changes:
- Rename the following command handler functions:
  - hagridctl::import::do_import -> hagridctl::import::run
  - hagrid::delete::delete:delete -> hagrid::delete::delete::run
    (still weird, but minus one delete. First delete in path is bin
     name, second - module for command handler)
  - tester::generate::do_generate -> tester::generate::run
  - tester::genreqs::do_genreqs -> tester::genreqs::run
2025-05-04 11:02:01 +02:00
Zeke Fast
5757ac2819 Migrate "hagrid-delete" from StructOpt to Clap v4 derive based API. Clean up code along the way.
Changes:
- Add "derive" and "unicode" features for "clap" in Cargo.toml.
- Remove dependency on "structopt" crate from Cargo.toml.
- Update Cargo.lock.
- Rename crate::delete::cli::Opt struct to Cli.
- Replace usage of #[structopt()] macros with #[derive(Parser)].
- Rename variables and function parameters from "opt" to "cli"
  accordingly.
- Don't parse opt.query in dispatch_cmd function. As Opt.query field
  type was replaced from String to Query in Cli.query new version of
  Clap automatically determines value_parser and calls FromStr::from_str
  implementation to convert str slice into field of type Query.
- Replace improrts of structopt::StructOpt with clap::Parser.
2025-05-04 11:02:01 +02:00
Zeke Fast
0027a25486 Format code. 2025-05-04 11:02:01 +02:00
Zeke Fast
012be246d1 Migrate "hagridctl" to Clap v4 derive based API. Clean up code along the way.
Changes:
- Add "derive" feature for "clap" in hagridctl/Cargo.toml.
- Move about description from hagridctl/src/cli.rs to "description" field in
  hagridctl/Cargo.toml. So, "clap" can access it via #[command(about)]
  macro configuration.
- Update Cargo.lock.
- Replace in hagridctl/src/cli.rs usage of clap::App from Clap 2.34.0
  with clap::Parser derive macro based API, cli::Cli struct and cli::Command
  enum.
- Omit explicit specification of args name which resulted in upper
  casing cmd option's parameters, e.g. "<keyring files>" -> "<KEYRING_FILES>".
- Replace in cli::dispatch_cmd() "if let" based logic with simple match
  on cli::Command enum variants.
- Get rid of manual parsing of options' and commands' values in
  cli::dispatch_cmd(). As it is hadled by derived
  value_parser (https://docs.rs/clap/latest/clap/_derive/index.html#arg-attributes)
  based on field types.
- Replace set of limited values specified with .possible_values() method
  call with #[arg(value_enum)] macro and hagridctl::cli::DeploymentEnvironment
  enum. I make environment names fully qualified, but left abbreviations
  as aliases, so `just hagridctl dev import` kind of commands still
  allowed.
- Replace in hagridctl/src/main.rs cli::app().get_matches() (which is Clap
  v2 API) with cli::Cli::parse() (Clap v4 API).
- Add "unicode" feature flag for clap dependency in hagridctl/Cargo.toml.
- Refactor config::load() and to use cli::Cli struct instead of
  clap::ArgMatches. Defaulting to Rocket.toml for configuration file was
  moved to default value for hagridctl::cli::Cli.env field.
- Replace .unwrap() calls in config::load() function with ? operator.
- Extract match statement from config::load() to
  HagridConfigs::get_by(deployment_environment) method.
2025-05-04 11:00:53 +02:00
Zeke Fast
48ef918d51 Migrate "hagrid-tester" to Clap v4 derive based API. Clean up code along the way.
Changes:
- Add "derive" feature for "clap" in tester/Cargo.toml.
- Move about description from tester/src/cli.rs to "description" field in
  tester/Cargo.toml. So, "clap" can access it via #[command(about)]
  macro configuration.
- Update Cargo.lock.
- Replace in tester/src/cli.rs usage of clap::App from Clap 2.34.0
  with clap::Parser derive macro based API, cli::Cli struct and cli::Command
  enum.
- Remove "--config/-c <FILE>" option for "tester" as it wasn't used and
  I believe it was a copy-paste from "hagridctl".
- Omit explicit specification of args name which resulted in upper
  casing cmd option's parameters, e.g. "<cert count>" -> "<CERT_COUNT>".
- Replace in cli::dispatch_cmd() "if let" based logic with simple match
  on cli::Command enum variants.
- Get rid of manual parsing of options' and commands' values in
  cli::dispatch_cmd(). As it is hadled by derived
  value_parser (https://docs.rs/clap/latest/clap/_derive/index.html#arg-attributes)
  based on field types.
- Make "fprs_path" parameter of tester::generate::do_generate function
  obligatory (previously it was Option<&Path>) as this argument has a
  default value to "fingerprints.txt" which is populated by clap. So, it
  never be None.
  In addition, clean up logic related to optional "fprs_path" parameter
  in tester::generate::do_generate function.
- Change signatures of the following functions to accept
  "impl AsRef<Path>" instead of &Path or Option<&Path>:
  - tester::generate::do_generate()
  - tester::genreqs::do_genreqs()
  That allows us to pass as function arguments anything from which Path
  can be borrowed, e.g. PathBuf without the need to explicitly transform
  the value at call site with calls like .as_path() or .as_deref().
- Replace in tester/src/main.rs cli::app().get_matches() (which is Clap
  v2 API) with cli::Cli::parse() (Clap v4 API).
2025-05-04 10:58:49 +02:00
Zeke Fast
333727a6dc Bump "clap" dependency version in Cargo.toml: "2" -> ">= 4.5.37".
Commands:

    cargo update clap@2.34.0

Additional Changes:
- Update Cargo.lock.
2025-05-04 10:58:49 +02:00
Zeke Fast
a7186bb6df Extract error printing logic from src/delete/main.rs to cli::print_errors function.
Changes:
- Extract error printing logic from src/delete/main.rs to cli::print_errors function.
- Move return error code from magic constant to crate::delete:ERROR_EXIT_CODE constant.
- Rename crate::delete::real_main() to crate::delete::run().
2025-05-04 10:58:48 +02:00
Zeke Fast
83b0515274 Extract "delete" action from src/delete/main.rs to its own module ("delete").
This is to better separate CLI handling, wiring and action logic.
2025-05-04 10:58:48 +02:00
Zeke Fast
b296157c08 Extract cmd dispatch code from src/delete/main.rs to delete::cli module.
Changes:
- Introduce cli::dispatch_cmd() function which just delegate to delete function.
2025-05-04 10:58:48 +02:00
Zeke Fast
abeafbe3d4 Move option parsing logic from src/delete/main.rs to newly introduced delete::cli module.
Changes:
- Introduce "delete::cli" module in "hagrid" crate.
- Move option parsing logic there from src/delete/main.rs (former src/delete.rs).
2025-05-04 10:58:47 +02:00
Zeke Fast
d35136b0d6 Promote src/delete.rs module to directory in "hagrid" crate.
Additional Changes:
- Change "hagrid-delete" bin declaration in Cargo.toml accordingly.
2025-05-04 10:58:47 +02:00
Zeke Fast
9acb4b52db Extract config related code in "hagridctl" crate from main.rs to "config" module.
Changes:
- Move the following structs from main.rs to "config" module:
  - HagridConfigs
  - HagridConfig
- Introduce "config::load" function and move configuration loading code into it.
- Adjust imports accordingly.
- Share ArgMatches returned by App::get_matches between
  config::load and cli::dispatch_cmd functions.
- Make HagridConfigs struct private to the config module as rest of the
  system doesn't need to know about it and interacts only with
  HagridConfig struct.
2025-05-04 10:58:47 +02:00
Zeke Fast
c0c8247c42 Extract CLI UI code in "hagridctl" crate from main.rs to "cli" module.
Additional Changes:
- Split "app" construction and command dispatch phases into different
  functions, hence app() and dispatch_cmd() functions.
2025-05-04 10:58:11 +02:00
Zeke Fast
f8982939aa Extract CLI UI code in "tester" crate from main.rs to "cli" module.
Additional Changes:
- Split "app" construction and command dispatch phases into different
  functions, hence app() and dispatch_cmd() functions.
2025-05-03 11:06:05 +02:00
Zeke Fast
b66fb67302 Add "just" recipes and aliases for runing bin-artifacts.
This allows for much more convenient cmd binary launching without the need
to remember distinction bitween binaries in main crate (hagrid) and
others (hagridctl, tester) and need to provide additional -- to cargo to
pass arguments to binaries.

Apart from convenience it also provide kinda "documentation" of binary
artifacts in the project as "run" group in just collect all the
artifacts together.

Changes:
- Add new named aliases for `just run` which runs "hagrid" binary with web
  server:
  - run-hagrid
  - hagrid
- Add new recipe to run "hagrid-delete" binary "run-hagrid-delete".
- Alias "run-hagrid-delete" recipe as
  - hagrid-delete
  - delete
- Add new recipe to run "hagridctl" crate with default binary
  "run-hagridctl".
- Alias "run-hagridctl" recipe as
  - hagridctl
- Add new recipe to run "tester" crate with default binary "run-tester".
- Alias "run-tester" recipe as
  - tester
2025-05-03 09:43:00 +02:00
Zeke Fast
84cfb5afaf Move development dependencies documentation from DEPENDENCIES.md to README.md.
Code review issue: https://gitlab.com/keys.openpgp.org/hagrid/-/merge_requests/213#note_2481434850
2025-05-02 16:04:52 +02:00
Zeke Fast
e966c1fbb7 Remove not used file contrib/hagrid-daily-backup.
Code review issue: https://gitlab.com/keys.openpgp.org/hagrid/-/merge_requests/213#note_2481434830
2025-05-02 16:04:52 +02:00
Zeke Fast
70b0eeb3e7 Introduce DEPENDENCIES.md file with list of software help for development. Reference DEPENDENCIES.md from README.md.
Changes:
- Document software dependencies for development process in
  DEPENDENCIES.md. file.
- Reference DEPENDENCIES.md from README.md.
2025-05-02 16:04:51 +02:00
Zeke Fast
7156d92246 Introduce justfile with bunch of recipes to streamline project development.
To install `just` follow instructions in
documentation https://just.systems/man/en/packages.html .

Full list of recipes and their description can be viewed by running
`just` command in project's root directory.

But here is the list of added commands for documentation sake:
- init                    # Perform initial setup of developer's system.
- init-rocket-config      # Copy Rocket's template configuration from Rocket.toml.disk to Rocket.toml. Rocket is Rust web framework. See https://rocket.rs/guide/v0.5/configuration/#configuration
- just-fmt                # Format justfile
- cargo-fmt               # Format Rust code in all packages (aka path based dependencies)
- fmt                     # Format all code [alias: f]
- just-lint-fmt           # Check justfile formatting
- cargo-lint-fmt          # Check Rust code formatting in all packages (aka path based dependencies)
- lint-fmt                # Check formatting of all code [alias: lf]
- clippy-lint             # Lint Rust code with Clippy [alias: cl]
- lint                    # Lint all code [alias: l]
- clippy-fix *args        # Apply Clippy's lint suggestions, i.e. fix Clippy linting warnings or errors
- cargo-fix *args         # Fix compilation warnings by applying compiler suggestions
- fix *args               # Fix lint and compilation warnings and errors. Pass given arguments to all sub-recipes, i.e. `just fix --allow-dirty` calls `just cargo-fix --allow-dirty` and `just clippy-fix --allow-dirty`.
- check                   # Check Rust code errors [alias: c]
- build                   # Compile Rust code [alias: b]
- test args='--workspace' # Run all tests (i.e. --workspace), but when args given pass them to `cargo test`, e.g. `just test fs::tests::init` [alias: t]
- run                     # Run web server [alias: r]
- watch-check *args       # Run continuous check of Rust code errors. Detect file changes and repeat check automatically. Ctrl+c to exit. You can pass additional arguments, e.g. --notify (-N). [alias: wc]
- watch-run *args         # Run web server and automatically restart on changes. Ctrl+c to exit. You can pass additional arguments, e.g. --notify (-N). [alias: wr]
- watch-test *args        # Run tests every time files changed. Ctrl+c to exit. You can pass additional arguments, e.g. --notify (-N). [alias: wt]
- clean                   # Clean compilartion artifacts (i.e. "target" directory)
- clean-translations      # Clean changes to translation files
- translate-templates     # Translate *.hbs templates of web pages
- db                      # Open database prompt
2025-05-02 16:04:51 +02:00
Zeke Fast
893f99c460 Improve error reporting in Zsh scripts by providing -euo pipefail options.
Documentation: https://linux.die.net/man/1/zshoptions
-e Exit on non-zero return code from commands
-u NO_UNSET
-o pipefail propagates piping errors
2025-05-01 00:24:11 +02:00
Zeke Fast
90d00637a0 Replace usage of deprecated cmd option "--all" with "--workspace" for "cargo test" command in .gitlab-ci.yml file.
`cargo help test` on 1.86.0 version says:

>       --all
>           Deprecated alias for --workspace.
2025-05-01 00:19:46 +02:00
Zeke Fast
3a3aba5db1 Replace redundant match expression with .unwrap_or_else. 2025-04-30 09:25:06 +02:00
Zeke Fast
0a829824dc Eliminate redundant conversions to Option and usage of filter_map. Use flat_map directly instead.
Documentation:
- https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.filter_map
- https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.flat_map

Additional Changes:
- Use point-less style and pass Email::try_from function directly to
  flat_map.
  flat_map passes uid to Email::try_from implicitly. That's why it's
  called point-less style. IMO it looks nicer!
2025-04-30 09:25:06 +02:00
Zeke Fast
51a66d643d Get rid of aliasing of "hagrid_database" as "database".
It improves code readability and consistency. In addition, less names
used in the code base.
2025-04-30 09:25:06 +02:00
Zeke Fast
2059c69226 Get rid of aliasing of "sequoia_openpgp" as "openpgp".
It improves code readability and consistency. In addition, less names
used in the code base.
2025-04-30 09:25:06 +02:00
Zeke Fast
0bea4f0f2a Remove imports of anyhow::Error and use named path anyhow::Error in place.
This is done for the same reason as for anyhow::Result and
core::result::Result types.

Error is whidely used name and IMO it usually good to keep it standard.
Apart from that I think assosicated type specification looks fore
readable when anyhow::Error is used in place. For example:

    type Error = Error;

was replaced with

    type Error = anyhow::Error;

Changes:
- Replace usage of imported unquolified Error type with anyhow::Error.
- Remove imports of anyhow::Error.
2025-04-30 09:25:05 +02:00
Zeke Fast
4d57dc1eb2 Replace imported anyhow::Result with usage of anyhow::Result in place.
This is to make this few files to much in style rest of code base.
2025-04-30 09:25:05 +02:00
Zeke Fast
0cfd412907 Clean up linting warnings about unused imports.
warning: unused import: `std::result`
     --> database/src/types.rs:3:5
      |
    3 | use std::result;
      |     ^^^^^^^^^^^
      |
      = note: `#[warn(unused_imports)]` on by default
2025-04-30 09:25:05 +02:00
Zeke Fast
9094b09b27 Format code. 2025-04-30 09:25:05 +02:00
Zeke Fast
c28e6af441 Restore default Result type to std::result::Result (aka core::result::Result).
Result it widely used type and the name is highly overloaded as almost
every library tend to define they own Result type.
When default Result type deviates from default it is usually harder to
read code and requires a bit of investigation work to understand this
particular Result corresponds to.

In Hagrid code base in many places anyhow::Result was made a default
type. It usually was imported through indirection, i.e.

    use anyhow::Result;

was imported in files like

- database/src/lib.rs
- hagridctl/src/import.rs
- hagridctl/src/main.rs
- src/main.rs

and in submodules that above import was reimported with something like

    use crate::Result;

or

    use super::Result;

So, I qualified such Result as anyhow::Result which IMO make code easier
to understand and "anyhow" is short enough name to use along with
Result.
All imports and reimports was cleaned up.

Changes:
- Full qualify anyhow's Result name as anyhow::Result in places where it
  is used to make code easier to read and understand.
- Remove imports and reimports of anyhow::Result.
- Restore default Result type to core::result::Result (aka std::result::Result).
- Clean up unnecesary name path qualification for Result type
  like "std::result" or "result" as it becomes a default one and doesn't
  overriden any more.
2025-04-30 09:25:04 +02:00
Zeke Fast
37e6b2cb09 Remove "lazy_static" crate dependency from Cargo.toml.
Additional Changes:
- Update Cargo.lock.
2025-04-29 23:28:30 +02:00
Zeke Fast
fefebaaffe Replace usage of lazy_static! macro with std::sync::LazyLock.
Now as we on new version of Rust (1.86.0) we can benefit from fairly
recent addition to std, like LazyLock. Nice benefit is one less crate to
depend on.

Documentation:
- https://crates.io/crates/lazy-static
- https://blog.rust-lang.org/2024/07/25/Rust-1.80.0/#lazycell-and-lazylock
- https://doc.rust-lang.org/stable/std/sync/struct.LazyLock.html

Not particularly useful for code changes, but might be helpful for thus
who want to navigate these Once, Lazy, Cell, Lock types and figure out
differences between them.

Additional Documentation:
- https://crates.io/crates/once_cell
- https://blog.rust-lang.org/2023/06/01/Rust-1.70.0/#oncecell-and-oncelock
- https://doc.rust-lang.org/stable/core/cell/index.html
- https://doc.rust-lang.org/stable/std/sync/struct.OnceLock.html

Changes:
- Move the following statics out of lazy_static! macro and initialize
  them using std::sync::LazyLock:
  - POPULAR_DOMAINS
  - KEY_UPLOAD
  - MAIL_SENT
  - KEY_ADDRESS_PUBLISHED
  - KEY_ADDRESS_UNPUBLISHED
- Remove lazy_static imports.
2025-04-29 23:28:30 +02:00
Zeke Fast
9bc3ccecac Format code (mostly imports).
Commands:

    cargo fmt --all
2025-04-28 00:38:39 +02:00
Zeke Fast
09072200d6 Replace "extern crate" and #[macro_use] declarations with "use" imports.
Both "extern crate" and #[macro_use] are artifacts of earlier editions
of Rust language. Nowadays we can entirely rely on "use" instead.

Changes:
- Replace "extern crate" with "use" imports.
- Replace

    #[macro_use]
    extern crate ...;

  declarations with "use" imports of used macros. For example,

    #[macro_use]
    extern crate anyhow;

  was replaced with

    use anyhow::anyhow;

  in every file where anyhow! macro were used.
- Favor direct usage of import path instead of aliased one.
  For example, in many places "sequoia_opengpg" were aliased as "openpgp",
  during imports replacements I tried to avoid usage of "openpgp" or
  introduced additional aliases (like "use sequoia_openpgp as openpgp")
  and used "sequoia_opengpg".
  I think this way it is easier to understand where name came from
  instead of search and jumping to lib.rs or main.rs files trying to
  find where name were aliased.
  Another example of such favoring is usage of "hagrid_database" over
  the "database" in imports.
  NOTE: the usage is still inconsistent and requires further clean up.
2025-04-27 23:36:59 +02:00
Zeke Fast
5399e6c2d3 Update CI image in .gitlab-ci.yml: "rust:1-bullseye" -> "rust:1.86-bookworm". 2025-04-27 18:48:29 +02:00
Zeke Fast
77372abb7c Add "rustfmt" and "clippy" to components in rust-toolchain.toml file.
Documentation:
- https://rust-lang.github.io/rustup/overrides.html#the-toolchain-file
- https://rust-lang.github.io/rustup/concepts/components.html
2025-04-27 18:31:58 +02:00
Zeke Fast
0200c15266 Format code.
Commands:

    cargo fmt --all
2025-04-27 18:12:57 +02:00
Zeke Fast
e4aac748be Clean up linting warnings: this manual char comparison can be written more succinctly.
Commands:

    cargo clippy --tests --no-deps --workspace

Warnings:

    warning: this manual char comparison can be written more succinctly
       --> src/web/vks_api.rs:117:37
        |
    117 |         .flat_map(|lang| lang.split(|c| c == '-' || c == ';' || c == '_').next())
        |                                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider using an array of `char`: `['-', ';', '_']`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#manual_pattern_char_comparison
        = note: `#[warn(clippy::manual_pattern_char_comparison)]` on by default
2025-04-27 18:04:41 +02:00
Zeke Fast
8b6049cb45 Clean up linting warnings: manual implementation of split_once.
Commands:

    cargo clippy --tests --no-deps --workspace

Warnings:

    warning: manual implementation of `split_once`
       --> src/mail.rs:340:17
        |
    340 |                 let mut it = line.splitn(2, ": ");
        |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    341 |                 let h = it.next().unwrap();
        |                 --------------------------- first usage here
    342 |                 let v = it.next().unwrap();
        |                 --------------------------- second usage here
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#manual_split_once
        = note: `#[warn(clippy::manual_split_once)]` on by default
    help: replace with `split_once`
        |
    340 ~                 let (h, v) = line.split_once(": ").unwrap();
    341 ~
    342 ~
        |
2025-04-27 17:53:56 +02:00
Zeke Fast
45402ddd07 Clean up linting warnings: unnecessary closure used with `bool::then.
Commands:

    cargo clippy --tests --no-deps --workspace

Warnings:

    warning: unnecessary closure used with `bool::then`
       --> src/dump.rs:125:21
        |
    125 |                     pp.decrypt(algo, &sk.session_key).is_ok().then(|| algo)
        |                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_lazy_evaluations
        = note: `#[warn(clippy::unnecessary_lazy_evaluations)]` on by default
    help: use `then_some` instead
        |
    125 -                     pp.decrypt(algo, &sk.session_key).is_ok().then(|| algo)
    125 +                     pp.decrypt(algo, &sk.session_key).is_ok().then_some(algo)
        |
2025-04-27 17:46:04 +02:00
Zeke Fast
e85e414619 Clean up linting warnings: allocating a new String only to create a temporary &str from it.
Commands:

    cargo clippy --tests --no-deps --workspace

Warnings:

    warning: allocating a new `String` only to create a temporary `&str` from it
       --> database/src/test.rs:109:37
        |
    109 |         let email = Email::from_str(&String::from_utf8(uid.value().to_vec()).unwrap()).unwrap();
        |                                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_to_owned
        = note: `#[warn(clippy::unnecessary_to_owned)]` on by default
    help: convert from `&[u8]` to `&str` directly
        |
    109 -         let email = Email::from_str(&String::from_utf8(uid.value().to_vec()).unwrap()).unwrap();
    109 +         let email = Email::from_str(core::str::from_utf8(uid.value()).unwrap()).unwrap();
        |

    warning: allocating a new `String` only to create a temporary `&str` from it
       --> database/src/test.rs:137:37
        |
    137 |         let email = Email::from_str(&String::from_utf8(uid.value().to_vec()).unwrap()).unwrap();
        |                                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_to_owned
    help: convert from `&[u8]` to `&str` directly
        |
    137 -         let email = Email::from_str(&String::from_utf8(uid.value().to_vec()).unwrap()).unwrap();
    137 +         let email = Email::from_str(core::str::from_utf8(uid.value()).unwrap()).unwrap();
        |
2025-04-27 17:36:39 +02:00
Zeke Fast
fcc9689ef3 Clean up linting warnings: manually reimplementing div_ceil.
Commands:

    cargo clippy --tests --no-deps --workspace

Warnings:

    warning: manually reimplementing `div_ceil`
      --> hagridctl/src/import.rs:58:22
       |
    58 |     let chunk_size = (input_files.len() + (num_threads - 1)) / num_threads;
       |                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider using `.div_ceil()`: `input_files.len().div_ceil(num_threads)`
       |
       = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#manual_div_ceil
       = note: `#[warn(clippy::manual_div_ceil)]` on by default
2025-04-27 17:26:52 +02:00
Zeke Fast
896206d6ca Clean up linting warnings: this if has identical blocks.
Commands:

    cargo clippy --tests --no-deps --workspace

Warnings:

    warning: this `if` has identical blocks
      --> database/src/openpgp_utils.rs:89:27
       |
    89 |           if !is_exportable {
       |  ___________________________^
    90 | |             false
    91 | |         } else if is_status_revoked(uid.revocation_status(&POLICY, None)) {
       | |_________^
       |
    note: same as this
      --> database/src/openpgp_utils.rs:91:75
       |
    91 |           } else if is_status_revoked(uid.revocation_status(&POLICY, None)) {
       |  ___________________________________________________________________________^
    92 | |             false
    93 | |         } else if let Ok(email) = Email::try_from(uid.userid()) {
       | |_________^
       = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#if_same_then_else
       = note: `#[warn(clippy::if_same_then_else)]` on by default
2025-04-27 17:11:24 +02:00
Zeke Fast
f4699a4545 Clean up linting warnings: accessing first element with index 0.
Commands:

    cargo clippy --tests --no-deps --workspace

Warnings:

    warning: accessing first element with `self.catalogs.get(0)`
      --> src/i18n.rs:21:32
       |
    21 |             .unwrap_or_else(|| self.catalogs.get(0).unwrap());
       |                                ^^^^^^^^^^^^^^^^^^^^ help: try: `self.catalogs.first()`
       |
       = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#get_first
       = note: `#[warn(clippy::get_first)]` on by default

    warning: accessing first element with `tpk_status
                     .email_status.get(0)`
       --> src/web/vks.rs:364:23
        |
    364 |       let primary_uid = tpk_status
        |  _______________________^
    365 | |         .email_status
    366 | |         .get(0)
        | |_______________^
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#get_first
    help: try
        |
    364 ~     let primary_uid = tpk_status
    365 +         .email_status.first()
        |
2025-04-27 16:59:14 +02:00
Zeke Fast
57a8e3a3a8 Clean up linting warnings: length comparison to zero.
Commands:

    cargo clippy --tests --no-deps --workspace

Warnings:

    warning: length comparison to zero
       --> database/src/sqlite.rs:518:17
        |
    518 |         assert!(db.merge(k1).unwrap().into_tpk_status().email_status.len() > 0);
        |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: using `!is_empty` is clearer and more explicit: `!db.merge(k1).unwrap().into_tpk_status().email_status.is_empty()`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#len_zero
        = note: `#[warn(clippy::len_zero)]` on by default

    warning: length comparison to zero
       --> database/src/sqlite.rs:520:13
        |
    520 | /             db.merge(k2.clone())
    521 | |                 .unwrap()
    522 | |                 .into_tpk_status()
    523 | |                 .email_status
    524 | |                 .len()
    525 | |                 > 0
        | |___________________^
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#len_zero
    help: using `!is_empty` is clearer and more explicit
        |
    520 ~             !db.merge(k2.clone())
    521 +                 .unwrap()
    522 +                 .into_tpk_status()
    523 +                 .email_status.is_empty()
        |

    warning: length comparison to zero
       --> database/src/sqlite.rs:529:13
        |
    529 | /             db.merge(k3.clone())
    530 | |                 .unwrap()
    531 | |                 .into_tpk_status()
    532 | |                 .email_status
    533 | |                 .len()
    534 | |                 > 0
        | |___________________^
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#len_zero
    help: using `!is_empty` is clearer and more explicit
        |
    529 ~             !db.merge(k3.clone())
    530 +                 .unwrap()
    531 +                 .into_tpk_status()
    532 +                 .email_status.is_empty()
        |

    warning: length comparison to zero
       --> src/dump.rs:999:24
        |
    999 |                     if reason.len() > 0 { ", " } else { "" },
        |                        ^^^^^^^^^^^^^^^^ help: using `!is_empty` is clearer and more explicit: `!reason.is_empty()`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#len_zero
        = note: `#[warn(clippy::len_zero)]` on by default
2025-04-27 16:51:28 +02:00
Zeke Fast
1f67668500 Clean up linting warnings: redundant return statements.
Commands:

    cargo clippy --tests --no-deps --workspace

Warnings:

    warning: unneeded `return` statement
       --> database/src/sqlite.rs:293:18
        |
    293 |             _ => return None,
        |                  ^^^^^^^^^^^
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_return
        = note: `#[warn(clippy::needless_return)]` on by default
    help: remove `return`
        |
    293 -             _ => return None,
    293 +             _ => None,
        |

    warning: unneeded `()`
       --> hagridctl/src/import.rs:153:25
        |
    153 |             _ => return (),
        |                         ^^ help: remove the `()`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#unused_unit
        = note: `#[warn(clippy::unused_unit)]` on by default

    warning: unneeded `()`
       --> hagridctl/src/import.rs:176:20
        |
    176 |             return ();
        |                    ^^ help: remove the `()`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#unused_unit

    warning: unneeded `return` statement
       --> src/web/hkp.rs:169:5
        |
    169 | /     return format!(
    170 | |         "Upload successful. Please note that identity information will only be published after verification. See {baseuri}/about/usage#gnupg-upload",
    171 | |         baseuri = origin.get_base_uri()
    172 | |     );
        | |_____^
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_return
        = note: `#[warn(clippy::needless_return)]` on by default
    help: remove `return`
        |
    169 ~     format!(
    170 +         "Upload successful. Please note that identity information will only be published after verification. See {baseuri}/about/usage#gnupg-upload",
    171 +         baseuri = origin.get_base_uri()
    172 ~     )
        |
2025-04-27 16:22:10 +02:00
Zeke Fast
27b68dc826 Clean up linting warnings: related to iterators, mostly replacement of flatten() with filter_map() or flat_map().
Commands:

    cargo clippy --tests --no-deps --workspace

Warnings:

    warning: called `Iterator::last` on a `DoubleEndedIterator`; this will needlessly iterate the entire iterator
       --> database/src/fs.rs:614:14
        |
    614 |             .last()
        |              ^^^^^^ help: try: `next_back()`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#double_ended_iterator_last
        = note: `#[warn(clippy::double_ended_iterator_last)]` on by default

    warning: called `map(..).flatten()` on `Iterator`
       --> database/src/fs.rs:708:18
        |
    708 |                   .map(Fingerprint::try_from)
        |  __________________^
    709 | |                 .flatten();
        | |__________________________^ help: try replacing `map` with `flat_map` and remove the `.flatten()`: `flat_map(Fingerprint::try_from)`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#map_flatten
        = note: `#[warn(clippy::map_flatten)]` on by default

    warning: called `map(..).flatten()` on `Iterator`
       --> database/src/sqlite.rs:424:18
        |
    424 |                   .map(|email| Email::from_str(email))
        |  __________________^
    425 | |                 .flatten()
        | |__________________________^ help: try replacing `map` with `flat_map` and remove the `.flatten()`: `flat_map(|email| Email::from_str(email))`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#map_flatten

    warning: called `map(..).flatten()` on `Iterator`
       --> database/src/sqlite.rs:422:18
        |
    422 |                   .map(|uid| uid.userid().email2().unwrap())
        |  __________________^
    423 | |                 .flatten()
        | |__________________________^ help: try replacing `map` with `filter_map` and remove the `.flatten()`: `filter_map(|uid| uid.userid().email2().unwrap())`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#map_flatten

    warning: called `map(..).flatten()` on `Iterator`
       --> database/src/sqlite.rs:431:18
        |
    431 |                   .map(|email| Email::from_str(&email.unwrap()))
        |  __________________^
    432 | |                 .flatten()
        | |__________________________^ help: try replacing `map` with `flat_map` and remove the `.flatten()`: `flat_map(|email| Email::from_str(&email.unwrap()))`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#map_flatten

    warning: called `map(..).flatten()` on `Iterator`
       --> database/src/sqlite.rs:453:18
        |
    453 |                   .map(Fingerprint::try_from)
        |  __________________^
    454 | |                 .flatten()
        | |__________________________^ help: try replacing `map` with `flat_map` and remove the `.flatten()`: `flat_map(Fingerprint::try_from)`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#map_flatten

    warning: called `map(..).flatten()` on `Iterator`
       --> database/src/lib.rs:266:14
        |
    266 |               .map(|binding| {
        |  ______________^
    267 | |                 if let Ok(email) = Email::try_from(binding.userid()) {
    268 | |                     Some((binding, email))
    269 | |                 } else {
    ...   |
    272 | |             })
    273 | |             .flatten()
        | |______________________^
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#map_flatten
    help: try replacing `map` with `filter_map` and remove the `.flatten()`
        |
    266 ~             .filter_map(|binding| {
    267 +                 if let Ok(email) = Email::try_from(binding.userid()) {
    268 +                     Some((binding, email))
    269 +                 } else {
    270 +                     None
    271 +                 }
    272 +             })
        |

    warning: called `map(..).flatten()` on `Iterator`
       --> database/src/lib.rs:315:22
        |
    315 |                       .map(|uid| Email::try_from(uid).ok())
        |  ______________________^
    316 | |                     .flatten()
        | |______________________________^ help: try replacing `map` with `filter_map` and remove the `.flatten()`: `filter_map(|uid| Email::try_from(uid).ok())`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#map_flatten

    warning: called `map(..).flatten()` on `Iterator`
       --> database/src/lib.rs:492:14
        |
    492 |               .map(|uid| Email::try_from(uid).ok())
        |  ______________^
    493 | |             .flatten()
        | |______________________^ help: try replacing `map` with `filter_map` and remove the `.flatten()`: `filter_map(|uid| Email::try_from(uid).ok())`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#map_flatten

    warning: called `map(..).flatten()` on `Iterator`
       --> database/src/lib.rs:509:14
        |
    509 |               .map(|binding| Email::try_from(binding.userid()))
        |  ______________^
    510 | |             .flatten()
        | |______________________^ help: try replacing `map` with `flat_map` and remove the `.flatten()`: `flat_map(|binding| Email::try_from(binding.userid()))`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#map_flatten

    warning: called `map(..).flatten()` on `Iterator`
       --> database/src/lib.rs:582:14
        |
    582 |               .map(|binding| Email::try_from(binding.userid()))
        |  ______________^
    583 | |             .flatten()
        | |______________________^ help: try replacing `map` with `flat_map` and remove the `.flatten()`: `flat_map(|binding| Email::try_from(binding.userid()))`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#map_flatten

    warning: called `map(..).flatten()` on `Iterator`
       --> database/src/lib.rs:590:14
        |
    590 |               .map(|binding| Email::try_from(binding.userid()))
        |  ______________^
    591 | |             .flatten()
        | |______________________^ help: try replacing `map` with `flat_map` and remove the `.flatten()`: `flat_map(|binding| Email::try_from(binding.userid()))`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#map_flatten

    warning: called `map(..).flatten()` on `Iterator`
       --> database/src/lib.rs:650:14
        |
    650 |               .map(|binding| Email::try_from(binding.userid()))
        |  ______________^
    651 | |             .flatten()
        | |______________________^ help: try replacing `map` with `flat_map` and remove the `.flatten()`: `flat_map(|binding| Email::try_from(binding.userid()))`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#map_flatten

    warning: called `map(..).flatten()` on `Iterator`
       --> database/src/lib.rs:708:10
        |
    708 |           .map(|binding| Email::try_from(binding.userid()))
        |  __________^
    709 | |         .flatten()
        | |__________________^ help: try replacing `map` with `flat_map` and remove the `.flatten()`: `flat_map(|binding| Email::try_from(binding.userid()))`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#map_flatten

    warning: useless conversion to the same type: `openpgp::cert::prelude::KeyAmalgamationIter<'_, openpgp::packet:🔑:PublicParts, openpgp::packet:🔑:UnspecifiedRole>`
       --> database/src/lib.rs:716:5
        |
    716 | /     tpk.keys()
    717 | |         .into_iter()
        | |____________________^ help: consider removing `.into_iter()`: `tpk.keys()`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#useless_conversion
        = note: `#[warn(clippy::useless_conversion)]` on by default

    warning: called `map(..).flatten()` on `Iterator`
      --> src/web/manage.rs:78:22
       |
    78 |                       .map(|u| u.userid().to_string().parse::<Email>())
       |  ______________________^
    79 | |                     .flatten()
       | |______________________________^ help: try replacing `map` with `flat_map` and remove the `.flatten()`: `flat_map(|u| u.userid().to_string().parse::<Email>())`
       |
       = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#map_flatten
       = note: `#[warn(clippy::map_flatten)]` on by default

    warning: called `map(..).flatten()` on `Iterator`
       --> src/web/vks.rs:225:10
        |
    225 |           .map(|address| address.parse::<Email>())
        |  __________^
    226 | |         .flatten()
        | |__________________^ help: try replacing `map` with `flat_map` and remove the `.flatten()`: `flat_map(|address| address.parse::<Email>())`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#map_flatten
2025-04-27 16:09:54 +02:00
Zeke Fast
535668c507 Clean up linting warnings: replace usage of explicit lifetimes with elision.
Commands:

    cargo clippy --tests --no-deps --workspace

Warnings:

    warning: the following explicit lifetimes could be elided: 'a
       --> database/src/fs.rs:348:6
        |
    348 | impl<'a> FilesystemTransaction<'a> {
        |      ^^                        ^^
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_lifetimes
        = note: `#[warn(clippy::needless_lifetimes)]` on by default
    help: elide the lifetimes
        |
    348 - impl<'a> FilesystemTransaction<'a> {
    348 + impl FilesystemTransaction<'_> {
        |

    warning: the following explicit lifetimes could be elided: 'a
       --> database/src/sqlite.rs:141:6
        |
    141 | impl<'a> DatabaseTransaction<'a> for SqliteTransaction {
        |      ^^                      ^^
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_lifetimes
    help: elide the lifetimes
        |
    141 - impl<'a> DatabaseTransaction<'a> for SqliteTransaction {
    141 + impl DatabaseTransaction<'_> for SqliteTransaction {
        |

    warning: the following explicit lifetimes could be elided: 'a
      --> src/dump.rs:40:6
       |
    40 | impl<'a> std::fmt::Display for SessionKeyDisplay<'a> {
       |      ^^                                          ^^
       |
       = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_lifetimes
       = note: `#[warn(clippy::needless_lifetimes)]` on by default
    help: elide the lifetimes
       |
    40 - impl<'a> std::fmt::Display for SessionKeyDisplay<'a> {
    40 + impl std::fmt::Display for SessionKeyDisplay<'_> {
       |
2025-04-27 15:09:23 +02:00
Zeke Fast
c77bf9d3db Clean up linting warnings: deref coersion related.
Commands:

    cargo clippy --tests --no-deps --workspace

Warnings:

    warning: called `.as_ref().map(|f| f.as_path())` on an `Option` value
      --> tester/src/main.rs:78:13
       |
    78 |             output_fprs.as_ref().map(|f| f.as_path()),
       |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider using as_deref: `output_fprs.as_deref()`
       |
       = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#option_as_ref_deref
       = note: `#[warn(clippy::option_as_ref_deref)]` on by default

    warning: deref which would be done by auto-deref
       --> src/web/vks_web.rs:358:52
        |
    358 |     for ValueField { name, value } in Form::values(&*String::from_utf8_lossy(&buf)) {
        |                                                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `&String::from_utf8_lossy(&buf)`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#explicit_auto_deref
        = note: `#[warn(clippy::explicit_auto_deref)]` on by default
2025-04-27 15:01:26 +02:00
Zeke Fast
fec6763b75 Clean up linting warnings: remove unnecessary borrowings.
Commands:

    cargo clippy --tests --no-deps --workspace

Warnings:

    warning: the borrowed expression implements the required traits
       --> database/src/fs.rs:258:40
        |
    258 |         let typ = fs::symlink_metadata(&path).ok()?.file_type();
        |                                        ^^^^^ help: change this to: `path`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_borrows_for_generic_args
        = note: `#[warn(clippy::needless_borrows_for_generic_args)]` on by default

    warning: the borrowed expression implements the required traits
       --> database/src/fs.rs:287:44
        |
    287 |             let typ = fs::symlink_metadata(&path)?.file_type();
        |                                            ^^^^^ help: change this to: `path`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_borrows_for_generic_args

    warning: the borrowed expression implements the required traits
       --> database/src/fs.rs:328:13
        |
    328 |     symlink(&symlink_content, &symlink_name_tmp)?;
        |             ^^^^^^^^^^^^^^^^ help: change this to: `symlink_content`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_borrows_for_generic_args

    warning: the borrowed expression implements the required traits
       --> database/src/fs.rs:329:31
        |
    329 |     rename(&symlink_name_tmp, &symlink_name)?;
        |                               ^^^^^^^^^^^^^ help: change this to: `symlink_name`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_borrows_for_generic_args

    warning: the borrowed expression implements the required traits
       --> database/src/fs.rs:334:35
        |
    334 |     if let Ok(target) = read_link(&link) {
        |                                   ^^^^^ help: change this to: `link`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_borrows_for_generic_args

    warning: this expression creates a reference which is immediately dereferenced by the compiler
       --> database/src/sqlite.rs:424:46
        |
    424 |                 .map(|email| Email::from_str(&email))
        |                                              ^^^^^^ help: change this to: `email`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_borrow
        = note: `#[warn(clippy::needless_borrow)]` on by default

    warning: this expression creates a reference which is immediately dereferenced by the compiler
       --> database/src/lib.rs:546:51
        |
    546 |                 self.set_email_unpublished_filter(&tx, &current_fpr, |uid| {
        |                                                   ^^^ help: change this to: `tx`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_borrow

    warning: this expression creates a reference which is immediately dereferenced by the compiler
       --> database/src/lib.rs:602:29
        |
    602 |         self.regenerate_wkd(&tx, fpr_primary, &published_tpk_clean)?;
        |                             ^^^ help: change this to: `tx`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_borrow

    warning: this expression creates a reference which is immediately dereferenced by the compiler
       --> hagridctl/src/import.rs:161:86
        |
    161 |                         db.set_email_published(&key_fpr.clone().try_into().unwrap(), &email)
        |                                                                                      ^^^^^^ help: change this to: `email`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_borrow
        = note: `#[warn(clippy::needless_borrow)]` on by default

    warning: this expression creates a reference which is immediately dereferenced by the compiler
      --> src/sealed_state.rs:20:37
       |
    20 |         let cipher = Aes256Gcm::new(&key);
       |                                     ^^^^ help: change this to: `key`
       |
       = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_borrow
       = note: `#[warn(clippy::needless_borrow)]` on by default

    warning: the borrowed expression implements the required traits
      --> src/template_helpers.rs:46:77
       |
    46 |                         remove_extension(remove_extension(path.strip_prefix(&template_path)?));
       |                                                                             ^^^^^^^^^^^^^^ help: change this to: `template_path`
       |
       = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_borrows_for_generic_args
       = note: `#[warn(clippy::needless_borrows_for_generic_args)]` on by default

    warning: the borrowed expression implements the required traits
       --> src/web/vks.rs:216:46
        |
    216 |         Err(e) => return UploadResponse::err(&e.to_string()),
        |                                              ^^^^^^^^^^^^^^ help: change this to: `e.to_string()`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_borrows_for_generic_args

    warning: the borrowed expression implements the required traits
       --> src/web/vks.rs:248:40
        |
    248 |             return UploadResponse::err(&format!("error sending email to {}", &email));
        |                                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: change this to: `format!("error sending email to {}", &email)`
        |
        = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_borrows_for_generic_args
2025-04-27 15:01:12 +02:00
Zeke Fast
80df057617 Clean up linting warnings: lines_filter_map_ok.
Commands:

    cargo clippy --tests --no-deps --workspace

Warnings:

    warning: `flatten()` will run forever if the iterator repeatedly produces an `Err`
      --> tester/src/genreqs.rs:12:70
       |
    12 |     let fingerprints: Vec<String> = io::BufReader::new(file).lines().flatten().collect();
       |                                                                      ^^^^^^^^^ help: replace with: `map_while(Result::ok)`
       |
    note: this expression returning a `std::io::Lines` may produce an infinite number of `Err` in case of a read error
      --> tester/src/genreqs.rs:12:37
       |
    12 |     let fingerprints: Vec<String> = io::BufReader::new(file).lines().flatten().collect();
       |                                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
       = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#lines_filter_map_ok
       = note: `#[warn(clippy::lines_filter_map_ok)]` on by default
2025-04-27 15:00:50 +02:00
Zeke Fast
5731f8aa2b Clean up linting warnings: redundant import.
Commands:

    cargo clippy --tests --no-deps --workspace

Warnings:

    warning: this import is redundant
      --> database/src/fs.rs:12:1
       |
    12 | use tempfile;
       | ^^^^^^^^^^^^^ help: remove it entirely
       |
       = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#single_component_path_imports
       = note: `#[warn(clippy::single_component_path_imports)]` on by default
2025-04-27 15:00:16 +02:00
Zeke Fast
62b936864d Run cargo fmt --all to format code to pass CI checks.
Commands:

    cargo fmt --all
2025-04-27 13:37:18 +02:00
Zeke Fast
4f86585ac3 Resolve dependency version differences for "sequoia-openpgp" crate.
It eliminates the following output of `cargo autoinherit` command:

    `sequoia-openpgp` won't be auto-inherited because there are multiple sources for it:
      - version: =1.17.0
      - version: ^1.17.0

Changes:
- Allow "sequoia-openpgp" crate to use workspace dependencies by resolving
  version differences constraints in Cargo.toml files.
2025-04-27 13:20:00 +02:00
Zeke Fast
8db33156c3 Resolve dependency version differences for "multipart" crate.
It eliminates the following output of `cargo autoinherit` command:

    `multipart` won't be auto-inherited because there are multiple sources for it:
      - version: ^0
      - version: ~0.18

Changes:
- Allow "multipart" crate to use workspace dependencies by resolving
  version differences constraints in Cargo.toml files.
2025-04-27 13:19:59 +02:00
Zeke Fast
b8fdaeb3c6 Unify crates dependency management in project's Cargo.toml using workspace dependencies.
This simplifies dependencies management and upgrades while ensuring that
dependencies version aligned with all the crates in the project and
neither dependency is used twice with different versions by accident
(though dependencies still can appear several times as sub-dependencies due to
 misaligned version constraints for dependency resolution).

Documentation and useful articles:
- https://mainmatter.com/blog/2024/03/18/cargo-autoinherit/
- https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#inheriting-a-dependency-from-a-workspace
- https://crates.io/crates/cargo-autoinherit

Commands:

    `cargo autoinherit`

Output:

    $ cargo autoinherit
    `multipart` won't be auto-inherited because there are multiple sources for it:
      - version: ^0.18.0
      - version: ^0
    `sequoia-openpgp` won't be auto-inherited because there are multiple sources for it:
      - version: ^1.17.0
      - version: =1.17.0

Changes:
- Collect all the dependencies for workspace's crates in the top level
  Cargo.toml file by applying `cargo autoinherit`.
- Use workspace dependencies in crates Cargo.toml files (i.e.
  crate_name = { workspace = true }).
2025-04-27 13:18:09 +02:00
Zeke Fast
7df4d76d5d Set "resolver" explicitly to "3".
Documentation: https://doc.rust-lang.org/cargo/reference/resolver.html#resolver-versions

Changes:
- Add explicit specification of "resolver" field to Cargo.toml. Set it
  to version "3".
2025-04-27 11:06:45 +02:00
Zeke Fast
e72c647505 Upgrade set explicit version requirement for "multipart" dependency in Cargo.toml: "0" -> "0.18".
`cargo build` gives the following warnings:

    warning: the following packages contain code that will be rejected by a future version of Rust: buf_redux v0.8.4, multipart v0.18.0, traitobject v0.1.0, typemap v0.3.3
    note: to see what the problems were, use the option `--future-incompat-report`, or run `cargo report future-incompatibilities --id 1`

which is when run with `cargo build --future-incompat-report` gives the
following:

    warning: `hagrid` (bin "hagrid") generated 9 warnings
        Finished `dev` profile [unoptimized + debuginfo] target(s) in 5.91s
    warning: the following packages contain code that will be rejected by a future version of Rust: buf_redux v0.8.4, multipart v0.18.0, traitobject v0.1.0, typemap v0.3.3
    note:
    To solve this problem, you can try the following approaches:

    - Some affected dependencies have newer versions available.
    You may want to consider updating them to a newer version to see if the issue has been fixed.

    traitobject v0.1.0 has the following newer versions available: 0.1.1

    - If the issue is not solved by updating the dependencies, a fix has to be
    implemented by those dependencies. You can help with that by notifying the
    maintainers of this problem (e.g. by creating a bug report) or by proposing a
    fix to the maintainers (e.g. by creating a pull request):

      - buf_redux@0.8.4
      - Repository: https://github.com/abonander/buf_redux
      - Detailed warning command: `cargo report future-incompatibilities --id 3 --package buf_redux@0.8.4`

      - multipart@0.18.0
      - Repository: http://github.com/abonander/multipart
      - Detailed warning command: `cargo report future-incompatibilities --id 3 --package multipart@0.18.0`

      - traitobject@0.1.0
      - Repository: https://github.com/reem/rust-traitobject.git
      - Detailed warning command: `cargo report future-incompatibilities --id 3 --package traitobject@0.1.0`

      - typemap@0.3.3
      - Repository: https://github.com/reem/rust-typemap
      - Detailed warning command: `cargo report future-incompatibilities --id 3 --package typemap@0.3.3`

    - If waiting for an upstream fix is not an option, you can use the `[patch]`
    section in `Cargo.toml` to use your own version of the dependency. For more
    information, see:
    https://doc.rust-lang.org/cargo/reference/overriding-dependencies.html#the-patch-section

    note: this report can be shown with `cargo report future-incompatibilities --id 1`

In attempt to fix the warning I set explict dependency for "multipart"
crate.

Changes:
- Set explicit version of "multipart" dependency in Cargo.toml: "0" -> "0.18.0".
- Update Cargo.lock: `cargo build`
2025-04-27 11:03:04 +02:00
Zeke Fast
d2ac58b3fa Explicitly set "rust-version" in Cargo.toml. Upgrade Rust version in clippy.toml: 1.58.1 -> 1.86.
Documentation:
- https://github.com/rust-lang/rust-clippy?tab=readme-ov-file#specifying-the-minimum-supported-rust-version
- https://doc.rust-lang.org/clippy/configuration.html#specifying-the-minimum-supported-rust-version
- https://doc.rust-lang.org/cargo/reference/manifest.html#the-rust-version-field
- https://doc.rust-lang.org/cargo/reference/rust-version.html

Setting "rust-version" in Cargo.toml might be redundant for Clippy, but
it may serve well for Cargo itself. So, I set it in both places.

Changes:
- Update "msrv" in clippy.toml: 1.58.1 -> 1.86.
- Explicitly set "rust-version" in Cargo.toml to "1.86".
2025-04-27 10:33:06 +02:00
Zeke Fast
c541c19622 Fix imports of test module after switch to 2024 edition.
Changes:
- Add "crate::" to the imports of "test" module in "fs" and "sqlite"
  modules of "database" crate.
2025-04-27 02:04:48 +02:00
Zeke Fast
58959e112e Fix compilation errors with binding modifier "ref" after switch to 2024 edition.
Fix the following and alike errors after switch to 2024 edition:

error: binding modifiers may only be written when the default binding mode is `move`
   --> database/src/fs.rs:552:27
    |
552 |             ByFingerprint(ref fp) => self.link_by_fingerprint(fp),
    |                           ^^^ binding modifier not allowed under `ref` default binding mode
    |
    = note: for more information, see <https://doc.rust-lang.org/nightly/edition-guide/rust-2024/match-ergonomics.html>
note: matching on a reference type with a non-reference pattern changes the default binding mode
   --> database/src/fs.rs:552:13
    |
552 |             ByFingerprint(ref fp) => self.link_by_fingerprint(fp),
    |             ^^^^^^^^^^^^^^^^^^^^^ this matches on type `&_`
help: remove the unnecessary binding modifier
    |
552 -             ByFingerprint(ref fp) => self.link_by_fingerprint(fp),
552 +             ByFingerprint(fp) => self.link_by_fingerprint(fp),
    |

error: binding modifiers may only be written when the default binding mode is `move`
   --> database/src/fs.rs:553:21
    |
553 |             ByKeyID(ref keyid) => self.link_by_keyid(keyid),
    |                     ^^^ binding modifier not allowed under `ref` default binding mode
    |
    = note: for more information, see <https://doc.rust-lang.org/nightly/edition-guide/rust-2024/match-ergonomics.html>
note: matching on a reference type with a non-reference pattern changes the default binding mode
   --> database/src/fs.rs:553:13
    |
553 |             ByKeyID(ref keyid) => self.link_by_keyid(keyid),
    |             ^^^^^^^^^^^^^^^^^^ this matches on type `&_`
help: remove the unnecessary binding modifier
    |
553 -             ByKeyID(ref keyid) => self.link_by_keyid(keyid),
553 +             ByKeyID(keyid) => self.link_by_keyid(keyid),
    |

error: binding modifiers may only be written when the default binding mode is `move`
   --> database/src/fs.rs:554:21
    |
554 |             ByEmail(ref email) => self.link_by_email(email),
    |                     ^^^ binding modifier not allowed under `ref` default binding mode
    |
    = note: for more information, see <https://doc.rust-lang.org/nightly/edition-guide/rust-2024/match-ergonomics.html>
note: matching on a reference type with a non-reference pattern changes the default binding mode
   --> database/src/fs.rs:554:13
    |
554 |             ByEmail(ref email) => self.link_by_email(email),
    |             ^^^^^^^^^^^^^^^^^^ this matches on type `&_`
help: remove the unnecessary binding modifier
    |
554 -             ByEmail(ref email) => self.link_by_email(email),
554 +             ByEmail(email) => self.link_by_email(email),
    |

error: binding modifiers may only be written when the default binding mode is `move`
   --> database/src/sqlite.rs:278:27
    |
278 |             ByFingerprint(ref fp) => query_simple(
    |                           ^^^ binding modifier not allowed under `ref` default binding mode
    |
    = note: for more information, see <https://doc.rust-lang.org/nightly/edition-guide/rust-2024/match-ergonomics.html>
note: matching on a reference type with a non-reference pattern changes the default binding mode
   --> database/src/sqlite.rs:278:13
    |
278 |             ByFingerprint(ref fp) => query_simple(
    |             ^^^^^^^^^^^^^^^^^^^^^ this matches on type `&_`
help: remove the unnecessary binding modifier
    |
278 -             ByFingerprint(ref fp) => query_simple(
278 +             ByFingerprint(fp) => query_simple(
    |

error: binding modifiers may only be written when the default binding mode is `move`
   --> database/src/sqlite.rs:283:21
    |
283 |             ByKeyID(ref keyid) => query_simple(
    |                     ^^^ binding modifier not allowed under `ref` default binding mode
    |
    = note: for more information, see <https://doc.rust-lang.org/nightly/edition-guide/rust-2024/match-ergonomics.html>
note: matching on a reference type with a non-reference pattern changes the default binding mode
   --> database/src/sqlite.rs:283:13
    |
283 |             ByKeyID(ref keyid) => query_simple(
    |             ^^^^^^^^^^^^^^^^^^ this matches on type `&_`
help: remove the unnecessary binding modifier
    |
283 -             ByKeyID(ref keyid) => query_simple(
283 +             ByKeyID(keyid) => query_simple(
    |

error: binding modifiers may only be written when the default binding mode is `move`
   --> database/src/sqlite.rs:288:21
    |
288 |             ByEmail(ref email) => query_simple(
    |                     ^^^ binding modifier not allowed under `ref` default binding mode
    |
    = note: for more information, see <https://doc.rust-lang.org/nightly/edition-guide/rust-2024/match-ergonomics.html>
note: matching on a reference type with a non-reference pattern changes the default binding mode
   --> database/src/sqlite.rs:288:13
    |
288 |             ByEmail(ref email) => query_simple(
    |             ^^^^^^^^^^^^^^^^^^ this matches on type `&_`
help: remove the unnecessary binding modifier
    |
288 -             ByEmail(ref email) => query_simple(
288 +             ByEmail(email) => query_simple(
    |

error: binding modifiers may only be written when the default binding mode is `move`
   --> database/src/lib.rs:194:27
    |
194 |             ByFingerprint(ref fp) => self.by_fpr(fp),
    |                           ^^^ binding modifier not allowed under `ref` default binding mode
    |
    = note: for more information, see <https://doc.rust-lang.org/nightly/edition-guide/rust-2024/match-ergonomics.html>
note: matching on a reference type with a non-reference pattern changes the default binding mode
   --> database/src/lib.rs:194:13
    |
194 |             ByFingerprint(ref fp) => self.by_fpr(fp),
    |             ^^^^^^^^^^^^^^^^^^^^^ this matches on type `&_`
help: remove the unnecessary binding modifier
    |
194 -             ByFingerprint(ref fp) => self.by_fpr(fp),
194 +             ByFingerprint(fp) => self.by_fpr(fp),
    |

error: binding modifiers may only be written when the default binding mode is `move`
   --> database/src/lib.rs:195:21
    |
195 |             ByKeyID(ref keyid) => self.by_kid(keyid),
    |                     ^^^ binding modifier not allowed under `ref` default binding mode
    |
    = note: for more information, see <https://doc.rust-lang.org/nightly/edition-guide/rust-2024/match-ergonomics.html>
note: matching on a reference type with a non-reference pattern changes the default binding mode
   --> database/src/lib.rs:195:13
    |
195 |             ByKeyID(ref keyid) => self.by_kid(keyid),
    |             ^^^^^^^^^^^^^^^^^^ this matches on type `&_`
help: remove the unnecessary binding modifier
    |
195 -             ByKeyID(ref keyid) => self.by_kid(keyid),
195 +             ByKeyID(keyid) => self.by_kid(keyid),
    |

error: binding modifiers may only be written when the default binding mode is `move`
   --> database/src/lib.rs:196:21
    |
196 |             ByEmail(ref email) => self.by_email(email),
    |                     ^^^ binding modifier not allowed under `ref` default binding mode
    |
    = note: for more information, see <https://doc.rust-lang.org/nightly/edition-guide/rust-2024/match-ergonomics.html>
note: matching on a reference type with a non-reference pattern changes the default binding mode
   --> database/src/lib.rs:196:13
    |
196 |             ByEmail(ref email) => self.by_email(email),
    |             ^^^^^^^^^^^^^^^^^^ this matches on type `&_`
help: remove the unnecessary binding modifier
    |
196 -             ByEmail(ref email) => self.by_email(email),
196 +             ByEmail(email) => self.by_email(email),
    |

Changes:
- Remove unnecessary "ref" binding modifiers in match statements as in
  later Rust editions so called match ergonomics modified binding
  behavior and "ref" when matching reference is not needed any more.
2025-04-27 02:04:47 +02:00
Zeke Fast
e90a2e2888 Fix broken import of HagridConfig after switch to 2024 edition in "hagridctl" crate.
Changes:
- Imports crate::HagridConfig instead of HagridConfig.
2025-04-27 02:04:47 +02:00
Zeke Fast
ee82a078ea Fix broken imports of Result after switch to 2024 edition in "database" crate.
As in 2015 edition we don't need to specify precise import path imports
like "use Result;" led to usage of imported in "lib.rs" anyhow::Result.
After switch to 2024 edition Rust requires to specify path precisely
(actually it starts require that in 2018 I guess).
So, after the switch compiler confuses "use Result;" with import of "use
core::result::Result;" and starts to require additional generic.
Simply pointing to "crate::Result" fixes the problem and points back to
anyhow::Result imported in lib.rs file.

The following compile errors were fixed:

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/types.rs:54:34
    |
54  |     fn try_from(uid: &UserID) -> Result<Self> {
    |                                  ^^^^^^ ---- supplied 1 generic argument
    |                                  |
    |                                  expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
54  |     fn try_from(uid: &UserID) -> Result<Self, E> {
    |                                             +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/sync.rs:17:44
    |
17  |     pub fn lock(path: impl AsRef<Path>) -> Result<Self> {
    |                                            ^^^^^^ ---- supplied 1 generic argument
    |                                            |
    |                                            expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
17  |     pub fn lock(path: impl AsRef<Path>) -> Result<Self, E> {
    |                                                       +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:59:59
    |
59  |     pub fn new_from_base(base_dir: impl Into<PathBuf>) -> Result<Self> {
    |                                                           ^^^^^^ ---- supplied 1 generic argument
    |                                                           |
    |                                                           expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
59  |     pub fn new_from_base(base_dir: impl Into<PathBuf>) -> Result<Self, E> {
    |                                                                      +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:72:10
    |
72  |     ) -> Result<Self> {
    |          ^^^^^^ ---- supplied 1 generic argument
    |          |
    |          expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
72  |     ) -> Result<Self, E> {
    |                     +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:81:10
    |
81  |     ) -> Result<Self> {
    |          ^^^^^^ ---- supplied 1 generic argument
    |          |
    |          expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
81  |     ) -> Result<Self, E> {
    |                     +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:281:10
    |
281 |     ) -> Result<()> {
    |          ^^^^^^ -- supplied 1 generic argument
    |          |
    |          expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
281 |     ) -> Result<(), E> {
    |                   +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:350:67
    |
350 |     fn link_email_vks(&self, email: &Email, fpr: &Fingerprint) -> Result<()> {
    |                                                                   ^^^^^^ -- supplied 1 generic argument
    |                                                                   |
    |                                                                   expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
350 |     fn link_email_vks(&self, email: &Email, fpr: &Fingerprint) -> Result<(), E> {
    |                                                                            +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:362:67
    |
362 |     fn link_email_wkd(&self, email: &Email, fpr: &Fingerprint) -> Result<()> {
    |                                                                   ^^^^^^ -- supplied 1 generic argument
    |                                                                   |
    |                                                                   expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
362 |     fn link_email_wkd(&self, email: &Email, fpr: &Fingerprint) -> Result<(), E> {
    |                                                                            +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:374:69
    |
374 |     fn unlink_email_vks(&self, email: &Email, fpr: &Fingerprint) -> Result<()> {
    |                                                                     ^^^^^^ -- supplied 1 generic argument
    |                                                                     |
    |                                                                     expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
374 |     fn unlink_email_vks(&self, email: &Email, fpr: &Fingerprint) -> Result<(), E> {
    |                                                                              +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:386:69
    |
386 |     fn unlink_email_wkd(&self, email: &Email, fpr: &Fingerprint) -> Result<()> {
    |                                                                     ^^^^^^ -- supplied 1 generic argument
    |                                                                     |
    |                                                                     expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
386 |     fn unlink_email_wkd(&self, email: &Email, fpr: &Fingerprint) -> Result<(), E> {
    |                                                                              +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:402:24
    |
402 |     fn commit(self) -> Result<()> {
    |                        ^^^^^^ -- supplied 1 generic argument
    |                        |
    |                        expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
402 |     fn commit(self) -> Result<(), E> {
    |                                 +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:406:48
    |
406 |     fn write_to_temp(&self, content: &[u8]) -> Result<Self::TempCert> {
    |                                                ^^^^^^ -------------- supplied 1 generic argument
    |                                                |
    |                                                expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
406 |     fn write_to_temp(&self, content: &[u8]) -> Result<Self::TempCert, E> {
    |                                                                     +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:415:76
    |
415 |     fn move_tmp_to_full(&self, file: Self::TempCert, fpr: &Fingerprint) -> Result<()> {
    |                                                                            ^^^^^^ -- supplied 1 generic argument
    |                                                                            |
    |                                                                            expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
415 |     fn move_tmp_to_full(&self, file: Self::TempCert, fpr: &Fingerprint) -> Result<(), E> {
    |                                                                                     +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:425:81
    |
425 |     fn move_tmp_to_published(&self, file: Self::TempCert, fpr: &Fingerprint) -> Result<()> {
    |                                                                                 ^^^^^^ -- supplied 1 generic argument
    |                                                                                 |
    |                                                                                 expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
425 |     fn move_tmp_to_published(&self, file: Self::TempCert, fpr: &Fingerprint) -> Result<(), E> {
    |                                                                                          +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:439:10
    |
439 |     ) -> Result<()> {
    |          ^^^^^^ -- supplied 1 generic argument
    |          |
    |          expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
439 |     ) -> Result<(), E> {
    |                   +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:454:73
    |
454 |     fn write_to_quarantine(&self, fpr: &Fingerprint, content: &[u8]) -> Result<()> {
    |                                                                         ^^^^^^ -- supplied 1 generic argument
    |                                                                         |
    |                                                                         expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
454 |     fn write_to_quarantine(&self, fpr: &Fingerprint, content: &[u8]) -> Result<(), E> {
    |                                                                                  +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:467:63
    |
467 |     fn link_email(&self, email: &Email, fpr: &Fingerprint) -> Result<()> {
    |                                                               ^^^^^^ -- supplied 1 generic argument
    |                                                               |
    |                                                               expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
467 |     fn link_email(&self, email: &Email, fpr: &Fingerprint) -> Result<(), E> {
    |                                                                        +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:478:65
    |
478 |     fn unlink_email(&self, email: &Email, fpr: &Fingerprint) -> Result<()> {
    |                                                                 ^^^^^^ -- supplied 1 generic argument
    |                                                                 |
    |                                                                 expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
478 |     fn unlink_email(&self, email: &Email, fpr: &Fingerprint) -> Result<(), E> {
    |                                                                          +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:484:74
    |
484 |     fn link_fpr(&self, from: &Fingerprint, primary_fpr: &Fingerprint) -> Result<()> {
    |                                                                          ^^^^^^ -- supplied 1 generic argument
    |                                                                          |
    |                                                                          expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
484 |     fn link_fpr(&self, from: &Fingerprint, primary_fpr: &Fingerprint) -> Result<(), E> {
    |                                                                                   +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:501:76
    |
501 |     fn unlink_fpr(&self, from: &Fingerprint, primary_fpr: &Fingerprint) -> Result<()> {
    |                                                                            ^^^^^^ -- supplied 1 generic argument
    |                                                                            |
    |                                                                            expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
501 |     fn unlink_fpr(&self, from: &Fingerprint, primary_fpr: &Fingerprint) -> Result<(), E> {
    |                                                                                     +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:528:33
    |
528 |     fn transaction(&'a self) -> Result<FilesystemTransaction<'a>> {
    |                                 ^^^^^^ ------------------------- supplied 1 generic argument
    |                                 |
    |                                 expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
528 |     fn transaction(&'a self) -> Result<FilesystemTransaction<'a>, E> {
    |                                                                 +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:536:78
    |
536 |     fn write_log_append(&self, filename: &str, fpr_primary: &Fingerprint) -> Result<()> {
    |                                                                              ^^^^^^ -- supplied 1 generic argument
    |                                                                              |
    |                                                                              expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
536 |     fn write_log_append(&self, filename: &str, fpr_primary: &Fingerprint) -> Result<(), E> {
    |                                                                                       +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:604:37
    |
604 |     fn get_last_log_entry(&self) -> Result<Fingerprint> {
    |                                     ^^^^^^ ----------- supplied 1 generic argument
    |                                     |
    |                                     expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
604 |     fn get_last_log_entry(&self) -> Result<Fingerprint, E> {
    |                                                       +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:624:10
    |
624 |     ) -> Result<Option<Fingerprint>> {
    |          ^^^^^^ ------------------- supplied 1 generic argument
    |          |
    |          expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
624 |     ) -> Result<Option<Fingerprint>, E> {
    |                                    +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:659:36
    |
659 |     fn check_consistency(&self) -> Result<()> {
    |                                    ^^^^^^ -- supplied 1 generic argument
    |                                    |
    |                                    expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
659 |     fn check_consistency(&self) -> Result<(), E> {
    |                                             +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/sqlite.rs:32:54
    |
32  |     pub fn new_file(base_dir: impl Into<PathBuf>) -> Result<Self> {
    |                                                      ^^^^^^ ---- supplied 1 generic argument
    |                                                      |
    |                                                      expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
32  |     pub fn new_file(base_dir: impl Into<PathBuf>) -> Result<Self, E> {
    |                                                                 +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/sqlite.rs:144:24
    |
144 |     fn commit(self) -> Result<()> {
    |                        ^^^^^^ -- supplied 1 generic argument
    |                        |
    |                        expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
144 |     fn commit(self) -> Result<(), E> {
    |                                 +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/sqlite.rs:150:48
    |
150 |     fn write_to_temp(&self, content: &[u8]) -> Result<Self::TempCert> {
    |                                                ^^^^^^ -------------- supplied 1 generic argument
    |                                                |
    |                                                expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
150 |     fn write_to_temp(&self, content: &[u8]) -> Result<Self::TempCert, E> {
    |                                                                     +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/sqlite.rs:154:76
    |
154 |     fn move_tmp_to_full(&self, file: Self::TempCert, fpr: &Fingerprint) -> Result<()> {
    |                                                                            ^^^^^^ -- supplied 1 generic argument
    |                                                                            |
    |                                                                            expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
154 |     fn move_tmp_to_full(&self, file: Self::TempCert, fpr: &Fingerprint) -> Result<(), E> {
    |                                                                                     +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/sqlite.rs:171:81
    |
171 |     fn move_tmp_to_published(&self, file: Self::TempCert, fpr: &Fingerprint) -> Result<()> {
    |                                                                                 ^^^^^^ -- supplied 1 generic argument
    |                                                                                 |
    |                                                                                 expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
171 |     fn move_tmp_to_published(&self, file: Self::TempCert, fpr: &Fingerprint) -> Result<(), E> {
    |                                                                                          +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/sqlite.rs:188:10
    |
188 |     ) -> Result<()> {
    |          ^^^^^^ -- supplied 1 generic argument
    |          |
    |          expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
188 |     ) -> Result<(), E> {
    |                   +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/sqlite.rs:200:75
    |
200 |     fn write_to_quarantine(&self, _fpr: &Fingerprint, _content: &[u8]) -> Result<()> {
    |                                                                           ^^^^^^ -- supplied 1 generic argument
    |                                                                           |
    |                                                                           expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
200 |     fn write_to_quarantine(&self, _fpr: &Fingerprint, _content: &[u8]) -> Result<(), E> {
    |                                                                                    +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/sqlite.rs:204:63
    |
204 |     fn link_email(&self, email: &Email, fpr: &Fingerprint) -> Result<()> {
    |                                                               ^^^^^^ -- supplied 1 generic argument
    |                                                               |
    |                                                               expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
204 |     fn link_email(&self, email: &Email, fpr: &Fingerprint) -> Result<(), E> {
    |                                                                        +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/sqlite.rs:221:65
    |
221 |     fn unlink_email(&self, email: &Email, fpr: &Fingerprint) -> Result<()> {
    |                                                                 ^^^^^^ -- supplied 1 generic argument
    |                                                                 |
    |                                                                 expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
221 |     fn unlink_email(&self, email: &Email, fpr: &Fingerprint) -> Result<(), E> {
    |                                                                          +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/sqlite.rs:231:78
    |
231 |     fn link_fpr(&self, from_fpr: &Fingerprint, primary_fpr: &Fingerprint) -> Result<()> {
    |                                                                              ^^^^^^ -- supplied 1 generic argument
    |                                                                              |
    |                                                                              expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
231 |     fn link_fpr(&self, from_fpr: &Fingerprint, primary_fpr: &Fingerprint) -> Result<(), E> {
    |                                                                                       +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/sqlite.rs:252:80
    |
252 |     fn unlink_fpr(&self, from_fpr: &Fingerprint, primary_fpr: &Fingerprint) -> Result<()> {
    |                                                                                ^^^^^^ -- supplied 1 generic argument
    |                                                                                |
    |                                                                                expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
252 |     fn unlink_fpr(&self, from_fpr: &Fingerprint, primary_fpr: &Fingerprint) -> Result<(), E> {
    |                                                                                         +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/sqlite.rs:264:33
    |
264 |     fn transaction(&'a self) -> Result<Self::Transaction> {
    |                                 ^^^^^^ ----------------- supplied 1 generic argument
    |                                 |
    |                                 expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
264 |     fn transaction(&'a self) -> Result<Self::Transaction, E> {
    |                                                         +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/sqlite.rs:268:80
    |
268 |     fn write_log_append(&self, _filename: &str, _fpr_primary: &Fingerprint) -> Result<()> {
    |                                                                                ^^^^^^ -- supplied 1 generic argument
    |                                                                                |
    |                                                                                expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
268 |     fn write_log_append(&self, _filename: &str, _fpr_primary: &Fingerprint) -> Result<(), E> {
    |                                                                                         +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/sqlite.rs:402:10
    |
402 |     ) -> Result<Option<Fingerprint>> {
    |          ^^^^^^ ------------------- supplied 1 generic argument
    |          |
    |          expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
402 |     ) -> Result<Option<Fingerprint>, E> {
    |                                    +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/sqlite.rs:411:36
    |
411 |     fn check_consistency(&self) -> Result<()> {
    |                                    ^^^^^^ -- supplied 1 generic argument
    |                                    |
    |                                    expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
411 |     fn check_consistency(&self) -> Result<(), E> {
    |                                             +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/sqlite.rs:476:37
    |
476 |     fn get_last_log_entry(&self) -> Result<Fingerprint> {
    |                                     ^^^^^^ ----------- supplied 1 generic argument
    |                                     |
    |                                     expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
476 |     fn get_last_log_entry(&self) -> Result<Fingerprint, E> {
    |                                                       +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/stateful_tokens.rs:14:50
    |
14  |     pub fn new(token_dir: impl Into<PathBuf>) -> Result<Self> {
    |                                                  ^^^^^^ ---- supplied 1 generic argument
    |                                                  |
    |                                                  expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
14  |     pub fn new(token_dir: impl Into<PathBuf>) -> Result<Self, E> {
    |                                                             +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/types.rs:97:29
    |
97  |     fn from_str(s: &str) -> Result<Email> {
    |                             ^^^^^^ ----- supplied 1 generic argument
    |                             |
    |                             expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
97  |     fn from_str(s: &str) -> Result<Email, E> {
    |                                         +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/types.rs:124:55
    |
124 |     fn try_from(fpr: sequoia_openpgp::Fingerprint) -> Result<Self> {
    |                                                       ^^^^^^ ---- supplied 1 generic argument
    |                                                       |
    |                                                       expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
124 |     fn try_from(fpr: sequoia_openpgp::Fingerprint) -> Result<Self, E> {
    |                                                                  +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/types.rs:163:29
    |
163 |     fn from_str(s: &str) -> Result<Fingerprint> {
    |                             ^^^^^^ ----------- supplied 1 generic argument
    |                             |
    |                             expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
163 |     fn from_str(s: &str) -> Result<Fingerprint, E> {
    |                                               +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/types.rs:196:55
    |
196 |     fn try_from(fpr: sequoia_openpgp::Fingerprint) -> Result<Self> {
    |                                                       ^^^^^^ ---- supplied 1 generic argument
    |                                                       |
    |                                                       expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
196 |     fn try_from(fpr: sequoia_openpgp::Fingerprint) -> Result<Self, E> {
    |                                                                  +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/types.rs:232:29
    |
232 |     fn from_str(s: &str) -> Result<KeyID> {
    |                             ^^^^^^ ----- supplied 1 generic argument
    |                             |
    |                             expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
232 |     fn from_str(s: &str) -> Result<KeyID, E> {
    |                                         +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:52:34
    |
52  | fn ensure_parent(path: &Path) -> Result<&Path> {
    |                                  ^^^^^^ ----- supplied 1 generic argument
    |                                  |
    |                                  expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
52  | fn ensure_parent(path: &Path) -> Result<&Path, E> {
    |                                              +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:319:60
    |
319 | fn symlink(symlink_content: &Path, symlink_name: &Path) -> Result<()> {
    |                                                            ^^^^^^ -- supplied 1 generic argument
    |                                                            |
    |                                                            expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
319 | fn symlink(symlink_content: &Path, symlink_name: &Path) -> Result<(), E> {
    |                                                                     +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:334:63
    |
334 | fn symlink_unlink_with_check(link: &Path, expected: &Path) -> Result<()> {
    |                                                               ^^^^^^ -- supplied 1 generic argument
    |                                                               |
    |                                                               expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
334 | fn symlink_unlink_with_check(link: &Path, expected: &Path) -> Result<(), E> {
    |                                                                        +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:268:48
    |
268 |     fn open_logfile(&self, file_name: &str) -> Result<File> {
    |                                                ^^^^^^ ---- supplied 1 generic argument
    |                                                |
    |                                                expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
268 |     fn open_logfile(&self, file_name: &str) -> Result<File, E> {
    |                                                           +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/fs.rs:280:55
    |
280 |         check: impl Fn(&Path, &Cert, &Fingerprint) -> Result<()>,
    |                                                       ^^^^^^ -- supplied 1 generic argument
    |                                                       |
    |                                                       expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
280 |         check: impl Fn(&Path, &Cert, &Fingerprint) -> Result<(), E>,
    |                                                                +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/sqlite.rs:65:56
    |
65  |     fn build_pool(manager: SqliteConnectionManager) -> Result<r2d2::Pool<SqliteConnectionManager>> {
    |                                                        ^^^^^^ ----------------------------------- supplied 1 generic argument
    |                                                        |
    |                                                        expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
65  |     fn build_pool(manager: SqliteConnectionManager) -> Result<r2d2::Pool<SqliteConnectionManager>, E> {
    |                                                                                                  +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/sqlite.rs:69:77
    |
69  |     fn new_internal(base_dir: PathBuf, manager: SqliteConnectionManager) -> Result<Self> {
    |                                                                             ^^^^^^ ---- supplied 1 generic argument
    |                                                                             |
    |                                                                             expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
69  |     fn new_internal(base_dir: PathBuf, manager: SqliteConnectionManager) -> Result<Self, E> {
    |                                                                                        +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/sqlite.rs:117:61
    |
117 |     fn start(pool: &r2d2::Pool<SqliteConnectionManager>) -> Result<Self> {
    |                                                             ^^^^^^ ---- supplied 1 generic argument
    |                                                             |
    |                                                             expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
117 |     fn start(pool: &r2d2::Pool<SqliteConnectionManager>) -> Result<Self, E> {
    |                                                                        +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/stateful_tokens.rs:24:66
    |
24  |     pub fn new_token(&self, token_type: &str, payload: &[u8]) -> Result<String> {
    |                                                                  ^^^^^^ ------ supplied 1 generic argument
    |                                                                  |
    |                                                                  expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
24  |     pub fn new_token(&self, token_type: &str, payload: &[u8]) -> Result<String, E> {
    |                                                                               +++

error[E0107]: enum takes 2 generic arguments but 1 generic argument was supplied
   --> database/src/stateful_tokens.rs:41:63
    |
41  |     pub fn pop_token(&self, token_type: &str, token: &str) -> Result<String> {
    |                                                               ^^^^^^ ------ supplied 1 generic argument
    |                                                               |
    |                                                               expected 2 generic arguments
    |
note: enum defined here, with 2 generic parameters: `T`, `E`
   --> .../.rustup/toolchains/1.86.0-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:528:10
    |
528 | pub enum Result<T, E> {
    |          ^^^^^^ -  -
help: add missing generic argument
    |
41  |     pub fn pop_token(&self, token_type: &str, token: &str) -> Result<String, E> {
    |                                                                            +++

Changes:
- Fix broken imports of Result in "database" crate by importing
  crate::Result instead of just Result.
2025-04-27 02:04:47 +02:00
Zeke Fast
50f80ebade Fix imports after migration to 2024 edition.
Differences in Rust edition lead to compilation errors like the ones
below:

error[E0432]: unresolved import `sync`
  --> database/src/fs.rs:15:5
   |
15 | use sync::FlockMutexGuard;
   |     ^^^^ help: a similar path exists: `crate::sync`
   |
   = note: `use` statements changed in Rust 2018; read more at <https://doc.rust-lang.org/edition-guide/rust-2018/module-system/path-clarity.html>

error[E0432]: unresolved import `types`
  --> database/src/fs.rs:16:5
   |
16 | use types::{Email, Fingerprint, KeyID};
   |     ^^^^^ help: a similar path exists: `crate::types`
   |
   = note: `use` statements changed in Rust 2018; read more at <https://doc.rust-lang.org/edition-guide/rust-2018/module-system/path-clarity.html>

error[E0432]: unresolved imports `Database`, `Query`
  --> database/src/fs.rs:18:6
   |
18 | use {Database, Query};
   |      ^^^^^^^^  ^^^^^ no external crate `Query`
   |      |
   |      no external crate `Database`
   |
   = help: consider importing this trait instead:
           crate::Database
   = help: consider importing this enum instead:
           crate::Query

error[E0432]: unresolved import `wkd`
  --> database/src/fs.rs:20:5
   |
20 | use wkd;
   |     ^^^ no external crate `wkd`
   |
help: consider importing this module instead
   |
20 | use crate::wkd;
   |     +++++++

error[E0432]: unresolved import `openpgp_utils`
  --> database/src/fs.rs:25:5
   |
25 | use openpgp_utils::POLICY;
   |     ^^^^^^^^^^^^^ help: a similar path exists: `crate::openpgp_utils`
   |
   = note: `use` statements changed in Rust 2018; read more at <https://doc.rust-lang.org/edition-guide/rust-2018/module-system/path-clarity.html>

error[E0432]: unresolved import `types`
  --> database/src/sqlite.rs:11:5
   |
11 | use types::{Email, Fingerprint, KeyID};
   |     ^^^^^ help: a similar path exists: `crate::types`
   |
   = note: `use` statements changed in Rust 2018; read more at <https://doc.rust-lang.org/edition-guide/rust-2018/module-system/path-clarity.html>

error[E0432]: unresolved imports `Database`, `Query`
  --> database/src/sqlite.rs:13:6
   |
13 | use {Database, Query};
   |      ^^^^^^^^  ^^^^^ no external crate `Query`
   |      |
   |      no external crate `Database`
   |
   = help: consider importing this trait instead:
           crate::Database
   = help: consider importing this enum instead:
           crate::Query

error[E0432]: unresolved import `Email`
 --> database/src/openpgp_utils.rs:9:5
  |
9 | use Email;
  |     ^^^^^ no external crate `Email`
  |
help: consider importing this struct through its public re-export instead
  |
9 | use crate::Email;
  |     +++++++

Changes:
- Prefix imports with "crate::" as compiler suggest to fix the errors.
2025-04-27 02:04:47 +02:00
Zeke Fast
709e358800 Change edition in Cargo.toml files to 2024.
Changes:
- Change edition in the following Cargo.toml files to 2024:
  - Cargo.toml change edition: 2018 -> 2024
  - Explicitly set 2024 (i.e. default 2015 -> 2024) edition
    in the following files:
    - database/Cargo.toml
    - hagridctl/Cargo.toml
    - tester/Cargo.toml
    NOTE: setting explicitly edition also clean up WARNINGS like ones bellow:

    warning: .../hagrid/database/Cargo.toml: no edition set: defaulting to the 2015 edition while the latest is 2024
    warning: .../hagrid/tester/Cargo.toml: no edition set: defaulting to the 2015 edition while the latest is 2024
    warning: .../hagrid/hagridctl/Cargo.toml: no edition set: defaulting to the 2015 edition while the latest is 2024
2025-04-27 02:04:46 +02:00
Zeke Fast
da6267887e Upgrade used Rust version in rust-toolchain.toml file: 1.82 -> 1.86. 2025-04-27 02:04:46 +02:00
Zeke Fast
31f4ff704f Migrate to newer toolchain configuration file: rust-toolchain -> rust-toolchain.toml.
Documentation: https://rust-lang.github.io/rustup/overrides.html#the-toolchain-file

Changes:
- Rename toolchain file: rust-toolchain -> rust-toolchain.toml.
- Format rust-toolchain.toml in TOML as required.
2025-04-27 02:04:46 +02:00
Vincent Breitmoser
871cae1e24 version 2.0.1 2025-03-25 09:23:57 +01:00
Vincent Breitmoser
f024b0bffe nix+web: pass in commit hash when building from flake 2025-03-25 09:22:50 +01:00
Vincent Breitmoser
a5294b07cb nginx: simplify routes for hagrid v2 2025-03-24 22:49:11 +01:00
Vincent Breitmoser
c6aa0b3fdb docker: remove
I no longer use this, so it won't be maintained
2025-03-24 22:48:39 +01:00
Vincent Breitmoser
3bceb608e8 fix wkd domain checker 2025-03-15 13:59:50 +01:00
Vincent Breitmoser
f6b1f3cc73 wkd: update a bit, and add to flake 2025-02-28 22:52:26 +01:00
Vincent Breitmoser
3293dd8f78 version 2.0 2025-02-28 22:07:16 +01:00
Vincent Breitmoser
c7a032eb69 nix: add nix flake 2025-02-28 22:05:32 +01:00
Vincent Breitmoser
475bcbffb8 nginx: route all requests via hagrid 2025-02-28 22:05:32 +01:00
Vincent Breitmoser
dafed3d492 db: don't use sq's export logic for our certs 2025-02-28 22:05:32 +01:00
Vincent Breitmoser
a504b0ea12 hagridctl: update for sqlite 2025-02-28 22:05:32 +01:00
Vincent Breitmoser
df6bfb2d84 db: improve typings for sqlite 2025-02-28 22:05:32 +01:00
Vincent Breitmoser
b5b5879474 db: add DatabaseTransaction abstraction 2025-02-28 22:05:32 +01:00
Vincent Breitmoser
5778aaed84 db: work on sqlite, make tests pass 2025-02-28 22:05:32 +01:00
Vincent Breitmoser
7beb5209af db: add sqlite query tracing during tests 2025-02-28 22:05:32 +01:00
puzzlewolf
4787816581 db: start work on rusqlite 2025-02-28 22:05:32 +01:00
Vincent Breitmoser
359475f89f docker: add sqlite dep 2025-02-28 22:05:32 +01:00
Vincent Breitmoser
253d672d47 db: abstract over log path interface 2025-02-28 22:05:32 +01:00
Vincent Breitmoser
e0aeef7ddc mail: support sending via local smtp server 2025-02-28 21:53:25 +01:00
Vincent Breitmoser
44db398a1c cargo: downgrade sequoia-openpgp to 1.17.0 (for now)
Starting with 1.18.0, the retain_userids method starts working
differently, returning an empty cert if no signed user ids or direct key
signature is left. Since we need this, we'll stay on 1.17.0 for now.
2024-11-17 19:08:23 +01:00
Vincent Breitmoser
8ea89d3e0e hagrid: fix tokens test 2024-11-17 14:15:34 +01:00
Vincent Breitmoser
0d25da7138 cargo: cargo fmt --all 2024-11-17 14:03:12 +01:00
Vincent Breitmoser
e0f8352ac6 docker: update docker-build for new rust-toolchain 2024-11-17 13:49:44 +01:00
Vincent Breitmoser
dca8afa1e6 cargo: cargo update 2024-11-17 13:47:22 +01:00
Vincent Breitmoser
ea44f52a16 rust-toolchain: update to 1.82.0 2024-11-17 13:46:25 +01:00
Vincent Breitmoser
b4d92f0ec1 use rust-crypto instead of ring for sealed state
Newer versions of ring are very obscure, and I couldn't figure out how
to use its interface in a reasonable time. I used the rust-crypto
methods instead where things were straightforward.
2024-11-17 13:46:24 +01:00
Vincent Breitmoser
26ef2f6e1c db: fix tests 2024-03-24 23:50:56 +01:00
Vincent Breitmoser
cfd9fd8eb3 tester: add gen-reqs command 2024-03-24 13:09:04 +01:00
Vincent Breitmoser
13ddd4ff3a tester: add tester workspace, adding tools for testing 2024-03-24 13:09:04 +01:00
Vincent Breitmoser
a9440c6d0a hagridctl: add dump command to dump entire database 2024-03-24 13:09:04 +01:00
Vincent Breitmoser
fe2337507a hagridctl: import public keys publishing emails 2024-03-24 13:09:04 +01:00
Vincent Breitmoser
36dff563fc docker: use bullseye base image 2024-03-24 13:09:04 +01:00
Vincent Breitmoser
da5648488b ci: actually use correct dep package name 2024-01-27 10:24:26 +01:00
Vincent Breitmoser
7f304929ea ci: update gitlab for openssl dep 2024-01-27 10:22:47 +01:00
Vincent Breitmoser
7c7b15e37c version 1.3.0 2024-01-26 15:35:37 +01:00
Vincent Breitmoser
dfafe5cdb7 cargo: use openssl crypo backend 2024-01-26 15:35:37 +01:00
Vincent Breitmoser
45c6fcf216 cargo: simplify versions of hagridctl as well 2024-01-26 15:33:41 +01:00
Vincent Breitmoser
e85d3eab40 nix: update for 23.11 2024-01-12 12:15:11 +01:00
Vincent Breitmoser
defd2314be docker: add instructions to build for a Debian environment via docker 2023-12-28 13:52:49 +01:00
Vincent Breitmoser
da4665306e cargo: cargo update
This update requires a forked version of rocket_i18n to accommodate for
a trivial renaming in rocket v0.5.0. Can be changed back to upstream if
https://github.com/Plume-org/rocket_i18n/pull/24 is merged.
2023-12-28 13:37:45 +01:00
Vincent Breitmoser
d11de8a354 hagrid: don't panic on short token size 2023-12-28 12:54:04 +01:00
Vincent Breitmoser
1d1eedc319 about: update privacy policy
Also clear translated files for this particular template.
2023-06-30 13:30:19 +02:00
Vincent Breitmoser
e7ec0edf1e db: check that user ids contain a valid self-signature for publication 2023-06-10 14:36:04 +00:00
Vincent Breitmoser
ed624da4a4 cargo: cargo update 2023-06-10 16:16:04 +02:00
Vincent Breitmoser
831331fd2d about: add news entry about k.o.o governance 2023-06-10 16:04:06 +02:00
Vic Demuzere
7f92f1813b Fix links to renamed Gitlab project
Most of them redirected to the new location but the commit link at the
bottom of every page resulted in a 404.
2023-06-05 11:37:36 +02:00
Vincent Breitmoser
37d42e96d7 i18n: fix some issues with position arguments 2023-02-11 17:57:54 +01:00
Vincent Breitmoser
e96594ab26 remove some stray println statements 2023-02-11 17:39:37 +01:00
Vincent Breitmoser
55e54c8bab readme: add notice about maintenance 2023-02-11 17:39:37 +01:00
Vincent Breitmoser
04b4bc817a i18n: update some fuzzily misdetected strings 2022-12-10 17:42:56 +01:00
Vincent Breitmoser
5e08a7086e cargo: cargo update and fix for some deps 2022-12-10 17:42:50 +01:00
Vincent Breitmoser
875ab41c7d cargo: apply cargo fmt --all 2022-12-10 15:29:58 +01:00
Justus Winter
dc2d67d9eb Bump sequoia-openpgp to 1.11 and synchronize src/dump.rs. 2022-12-09 12:59:42 +01:00
Vincent Breitmoser
0a28c04f86 contrib: remove systemd service file 2022-06-29 10:57:13 +02:00
Vincent Breitmoser
aca0cdcdff version 1.2.1 2022-04-09 13:37:51 +02:00
Vincent Breitmoser
aa6474fd29 cargo: cargo update 2022-04-09 13:36:33 +02:00
Vincent Breitmoser
55ec155b30 mail: update to lettre-0.10.0-rc.5 and adapt 2022-04-09 13:36:33 +02:00
Vincent Breitmoser
39fae28f53 version 1.2.0 2022-03-12 15:30:51 +01:00
Vincent Breitmoser
427fe351d8 i18n: tx pull 2022-03-12 15:20:12 +01:00
Vincent Breitmoser
7e00c56a24 dist: small css fixes 2022-03-10 18:09:10 +01:00
Nora Widdecke
07bab9d11b hagridtcl: Allow clippy::needless_collect 2022-03-06 23:30:12 +00:00
Nora Widdecke
e00cae5a4e db,hagridctl: Autofix clippy issues 2022-03-06 23:30:12 +00:00
Nora Widdecke
1802cc6811 Run clippy on the whole workspace 2022-03-06 23:30:12 +00:00
Nora Widdecke
59c42c033d Apply clippy to the tests, too 2022-03-06 23:30:12 +00:00
Nora Widdecke
ed924f439b hkp: Cleanup pks/lookup route.
- Replace custom FromRequest implementation and query parsing with
    rocket builtin.

  - Remove Hkp::Invalid variant, replaced with Result
2022-02-28 17:47:41 +01:00
Nora Widdecke
3b2810dcf7 hkp: Fix pks/lookup with urlencoded parameter.
And test the pks/lookup route with a urlencoded parameter.

  Fixes #168.
2022-02-28 17:42:22 +01:00
Vincent Breitmoser
7b8f5e1462 i18n: commit translation files for rustfmt determined compilation order 2022-02-26 18:47:32 +01:00
Vincent Breitmoser
a9c4786d14 i18n: extract include_i18n macro use into method for consistent compilation order 2022-02-26 18:23:22 +01:00
Vincent Breitmoser
e4718d7598 ci: simplify ci pipeline, check formatting 2022-02-26 18:20:55 +01:00
Vincent Breitmoser
b29845b893 cargo: apply cargo fmt --all 2022-02-26 17:01:14 +01:00
Vincent Breitmoser
961559e154 cargo: remove rustfmt config 2022-02-26 17:01:14 +01:00
Percy
4d8bb36824 fix: the text in the search button is not displayed in the small screen 2022-02-26 15:44:33 +00:00
Percy
2ec10fc40a fix: link and image overflow 2022-02-26 15:44:33 +00:00
Nora Widdecke
c1a88f8840 web: handle wkd requests 2022-02-26 16:40:54 +01:00
Vincent Breitmoser
329a9c09b0 update gettext-macros to 0.6.1 2022-02-26 16:34:57 +01:00
Nora Widdecke
36b03ea608 ci: Run clippy 2022-02-25 10:25:26 +01:00
Nora Widdecke
b06c2c96bd lint: allow clippy::nonminimal_bool 2022-02-25 10:25:26 +01:00
Nora Widdecke
46646f1965 lint: raise too-many-arguments-threshold 2022-02-25 10:25:26 +01:00
Nora Widdecke
a329a1a89a lint: fix clippy::single_component_path_imports 2022-02-25 10:25:26 +01:00
Nora Widdecke
cd020cae40 lint: fix clippy::collapsible_if 2022-02-25 10:25:26 +01:00
Nora Widdecke
b2a7ca29b7 lint: fix clippy::clone_on_copy 2022-02-25 10:25:26 +01:00
Nora Widdecke
5a6d1a97fd lint: fix clippy::bind_instead_of_map 2022-02-25 10:25:26 +01:00
Nora Widdecke
d2f6d682ac lint: fix clippy::needless_lifetimes 2022-02-25 10:25:26 +01:00
Nora Widdecke
4d27f3f5b9 lint: fix clippy::single_match 2022-02-25 10:25:26 +01:00
Nora Widdecke
a46bd4ebee lint: fix clippy::redundant_closure 2022-02-25 10:25:26 +01:00
Nora Widdecke
3253f50127 lint: fix clippy::match_like_matches_macro 2022-02-25 10:25:26 +01:00
Nora Widdecke
a2ace61e71 lint: fix clippy::or_fun_call 2022-02-25 10:25:26 +01:00
Nora Widdecke
421f8a0908 lint: Apply clippy autofixes
clippy::needless_borrow

  clippy::single_char_pattern

  clippy::redundant_clone

  clippy::needless_return

  clippy::needless_question_mark

  clippy::useless_conversion

  clippy::to_string_in_format_args

  clippy::to_string_in_format_args

  clippy::useless_format

  clippy::useless_vec

  clippy::toplevel_ref_arg

  clippy::redundant_static_lifetimes

  clippy::try_err
2022-02-25 10:23:34 +01:00
Nora Widdecke
8eb3984560 cargo: use rocket_prometheus 0.10.0-rc.1
Fixes #167
2022-02-15 11:35:13 +01:00
Vincent Breitmoser
546e3b9452 readme: update for stable rust 2022-02-06 23:33:33 +01:00
Justus Winter
57efbe2937 hagridctl: drop backtrace from error messages 2022-02-06 22:58:29 +01:00
Justus Winter
81b5426544 hagridctl: drop feature and recursion_limit attribute 2022-02-06 22:58:29 +01:00
Justus Winter
f1078b3ccc hagridctl: adapt to rockets new profile names 2022-02-06 22:58:29 +01:00
Justus Winter
42260ff2e1 web: change profile names to match rocket's new convention
see https://rocket.rs/v0.5-rc/guide/configuration/#default-provider
2022-02-06 22:58:29 +01:00
Justus Winter
7b413150ca web: start from rocket's default config for the tests 2022-02-06 22:58:29 +01:00
Justus Winter
11f93c3249 web: fix extracting configuration values 2022-02-06 22:58:29 +01:00
Justus Winter
fec0cc4852 web: port to handlebars 3, the version used in rocket's dyn templates 2022-02-06 22:58:29 +01:00
Justus Winter
b97a06f51c web: rename request_origin -> origin, it is a mouthful already 2022-02-06 22:58:29 +01:00
Justus Winter
cf0abbe047 web: immutable responses 2022-02-06 22:58:29 +01:00
Justus Winter
2bf703a796 web: don't use consumed response 2022-02-06 22:58:29 +01:00
Justus Winter
a31d69d111 web: enable prometheus again 2022-02-06 22:58:29 +01:00
Justus Winter
b428116189 web: i18n and request origin everywhere 2022-02-06 22:58:29 +01:00
Justus Winter
24eb0b0d1b web: anyhow::Error doesn't implement Responder 2022-02-06 22:58:29 +01:00
Justus Winter
23fb3f9fb2 web: fix header composition 2022-02-06 22:58:29 +01:00
Justus Winter
f50ce6912a cargo: use published rocket_i18n, hyperx 2022-02-06 22:58:29 +01:00
Vincent Breitmoser
a2bc5f014c web: first iteration, update to rocket v0.5-rc1 2022-02-06 22:58:29 +01:00
Vincent Breitmoser
3f156ec8c2 cargo: update, and use rocket v0.5-rc1 2022-02-06 22:58:22 +01:00
Vincent Breitmoser
33224d1855 i18n: fix tests and strings 2022-01-04 15:55:01 +01:00
Vincent Breitmoser
23880d1386 db+web: remove x-accel optimization
This removes a shortcut to serve certificates from nginx by including an
X-Accel-Redirect header in the response.
2022-01-04 13:52:58 +01:00
Vincent Breitmoser
77407e03cc db: correctly abstract NamedTempFile as type trait 2022-01-04 13:28:26 +01:00
Vincent Breitmoser
6782c57520 fs: group abstract methods on top of Database trait 2022-01-04 13:23:37 +01:00
Vincent Breitmoser
0e0b5c160a cargo: cargo update, and use recent nightly 2022-01-04 12:24:44 +01:00
Vincent Breitmoser
3aa26c10f3 nix: update shell.nix 2022-01-04 12:17:18 +01:00
Vincent Breitmoser
9d5ec287a9 cargo: update gettext-macros to patched 0.6
This should work with rust stable, once we update rocket.

Uses a patched version of gettext-macros with a bugfix for
https://github.com/Plume-org/gettext-macros/issues/16
2022-01-04 12:06:16 +01:00
Vincent Breitmoser
43cdb28b97 i18n: tx pull 2021-11-28 14:32:14 +01:00
Justus Winter
b8ddf58977 update sequoia-openpgp to 1.5 2021-10-25 16:04:12 +02:00
Justus Winter
6db41b87f2 update dump.rs from sq, revert to its canonical form
Previously, the code was taken with the modifications from
dump.sequoia-pgp.org.  However, the canonical form is the one from the
sq tool.
2021-10-11 11:06:13 +02:00
Justus Winter
8dabd2c37a update sequoia-openpgp to 1.4 2021-10-11 10:23:45 +02:00
Vincent Breitmoser
90356ddb28 update changed files from rebuild 2021-07-21 09:44:43 +02:00
Justus Winter
bb9a3d8324 Strip non-exportable signatures and cert components.
If non-exportable signatures are uploaded to Hagrid, this is most
certainly an accident.  Handle this gracefully by stripping these
signatures (and certificate components that are only bound by
non-exportable signatures) when writing them to the database.

Fixes #155.
2021-07-15 19:21:44 +00:00
Vincent Breitmoser
0543e13b14 nginx: re-add missing proxy cache path directive 2021-07-14 12:32:14 +02:00
Vincent Breitmoser
3432fbe584 readme: small update
Mention use of ngx_http_lua_module for nginx config, and document move
of the IRC channel to OFTC.
2021-07-13 11:09:00 +02:00
Vincent Breitmoser
569a9df5a0 nginx: update nginx.conf, ditch nginx-site.conf 2021-07-13 11:05:44 +02:00
173 changed files with 14155 additions and 9308 deletions

1
.envrc
View File

@@ -1 +1,2 @@
watch_file rust-toolchain
use nix

2
.gitignore vendored
View File

@@ -7,3 +7,5 @@
target
*.po~
/dist/templates/localized
result

View File

@@ -1,14 +1,11 @@
stages:
- build
build:binary:
stage: build
tags:
- docker
image: "rustlang/rust:nightly"
build, test and lint:
image: "nixos/nix:2.3.16"
interruptible: true
script:
- apt update -qy
- apt install -qy libclang-dev build-essential pkg-config clang nettle-dev gettext zsh
- ./make-translated-templates
- RUST_BACKTRACE=full cargo build
- RUST_BACKTRACE=full cargo test --all
- nix-shell --command true
- nix-shell --command ./make-translated-templates
- nix-shell --command cargo build
- nix-shell --command cargo clippy --tests --no-deps --workspace
- nix-shell --command cargo fmt --all -- --check
- nix-shell --command cargo test --all
- nix-shell --command cargo test --workspace

4211
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,72 +1,104 @@
[package]
name = "hagrid"
version = "1.1.0"
version = "2.1.0"
authors = ["Vincent Breitmoser <look@my.amazin.horse>", "Kai Michaelis <kai@sequoia-pgp.org>", "Justus Winter <justus@sequoia-pgp.org>"]
build = "build.rs"
default-run = "hagrid"
edition = "2018"
edition = "2024"
rust-version = "1.86"
resolver = "3"
[workspace]
members = [
"database",
"hagridctl",
"tester",
]
[dependencies]
hagrid-database = { path = "database" }
chrono = "0.4.10"
[workspace.dependencies]
anyhow = "1"
rocket = "0"
rocket_codegen = "0"
sequoia-openpgp = { version = "1.3", default-features = false, features = ["crypto-nettle"] }
multipart = "0"
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
time = "0.1"
tempfile = "3.0"
structopt = "0.2"
url = "1.6"
handlebars = "1.1.0"
num_cpus = "1.0"
ring = "0.13"
hagrid-database = { path = "database" }
aes-gcm = "0.10"
base64 = "0.10"
uuid = { version = "0.7", features = [ "v4" ] }
rocket_prometheus = "0.2"
lazy_static = "1.3.0"
rocket_i18n = "0.4"
gettext-macros = "0.5"
runtime-fmt = "0.4"
chrono = "0.4"
clap = ">= 4.5.37"
fs2 = "0.4"
gettext = "0.4"
gettext-macros = "0.6"
gettext-utils = "0.1"
glob = "0.3"
rfc2047 = "0.1"
hex = "0.3"
hyperx = "1.4"
idna = "0.1"
indicatif = "0.11"
lettre = { version = "=0.10.0-rc.5", default-features = false }
log = ">= 0.4.27"
multipart = "~0.18"
num_cpus = "1"
pathdiff = "0.1"
r2d2 = "0.8"
r2d2_sqlite = "0.24"
rand = "0.6"
regex = "1"
rocket = ">= 0.5.1"
rocket_codegen = ">= 0.5.1"
rocket_dyn_templates = ">= 0.2.0"
rocket_i18n = { git = "https://github.com/Valodim/rocket_i18n", branch = "go-async", default-features = false }
rocket_prometheus = ">= 0.10.1"
rusqlite = "0.31"
self_cell = "1"
serde = "1.0"
serde_derive = "1"
serde_json = "1"
sha2 = "0.10"
tempfile = "3"
time = "0.1"
toml = "0.5"
url = "1"
uuid = "0.7"
vergen = "3"
walkdir = "2"
zbase32 = "0.1"
sequoia-openpgp = { version = "=1.17.0", default-features = false }
rstest = ">= 0.26.1"
[patch.crates-io]
runtime-fmt = { git = "https://github.com/Valodim/runtime-fmt", rev = "44c15d832cb327ef33f95548a9a964d98c006fe4" }
[dependencies.lettre]
version = "0.10.0-pre"
default-features = false
# smtp-transport doesn't build (openssl problem)
features = ["builder", "file-transport", "sendmail-transport"]
git = "https://github.com/lettre/lettre"
rev = "245c600c82ee18b766e8729f005ff453a55dce34"
[dependencies.rocket_contrib]
version = "0"
default-features = false
features = ["handlebars_templates", "json"]
[dependencies]
hagrid-database = { workspace = true }
chrono = { workspace = true }
anyhow = { workspace = true }
rocket = { workspace = true, features = ["json"] }
rocket_dyn_templates = { workspace = true, features = ["handlebars"] }
rocket_codegen = { workspace = true }
sequoia-openpgp = { workspace = true, features = ["crypto-openssl"] }
multipart = { workspace = true }
serde = { workspace = true }
serde_derive = { workspace = true }
serde_json = { workspace = true }
time = { workspace = true }
tempfile = { workspace = true }
url = { workspace = true }
num_cpus = { workspace = true }
aes-gcm = { workspace = true }
sha2 = { workspace = true }
base64 = { workspace = true }
uuid = { workspace = true, features = ["v4"] }
rocket_prometheus = { workspace = true }
gettext-macros = { workspace = true }
gettext-utils = { workspace = true }
gettext = { workspace = true }
glob = { workspace = true }
hyperx = { workspace = true }
# this is a slightly annoying update, so keeping this back for now
lettre = { workspace = true, features = ["builder", "file-transport", "sendmail-transport", "smtp-transport"] }
rocket_i18n= { workspace = true, features = ["rocket"] }
[build-dependencies]
vergen = "3"
vergen = { workspace = true }
[dev-dependencies]
regex = "1"
regex = { workspace = true }
rstest = { workspace = true }
[[bin]]
name = "hagrid"
path = "src/main.rs"
[[bin]]
name = "hagrid-delete"
path = "src/delete.rs"

View File

@@ -6,6 +6,15 @@ Hagrid is a verifying OpenPGP key server.
You can find general instructions and an API documentation at the running
instance at [https://keys.openpgp.org](https://keys.openpgp.org).
Please note that Hagrid is built and maintained only for the service at
keys.openpgp.org. It is not maintained or officially supported as
deployable software.
Compatibility note: Hagrid v2.0 uses an sqlite certificate store instead of the
previous file based database. This means that it also no longer supports serving
certificates directly via reverse proxy. You can us hagridctl to dump and import
an old database.
License
-------
@@ -26,22 +35,20 @@ License along with Hagrid. If not, see
Quick Start
-----------
Building Hagrid requires a working nightly Rust toolchain. The
easiest way to get the toolchain is to download [rustup](https://rustup.rs).
Building Hagrid requires a working stable Rust toolchain.
The easiest way to get the toolchain is to download [rustup](https://rustup.rs).
Additionally, install external dependencies are required. Get them (on Debian or
Ubuntu) with
Additionally, some external dependencies are required.
Get them (on Debian or Ubuntu) with
```bash
sudo apt install gnutls-bin nettle-dev gcc llvm-dev libclang-dev build-essential pkg-config gettext
sudo apt install gnutls-bin libssl-dev gcc llvm-dev libclang-dev build-essential pkg-config gettext libsqlite3-dev
```
After rustup and all other dependencies are installed, get the nightly compiler and tools, copy the
config file, and simply compile and run:
After Rust and the other dependencies are installed, copy the config file (or run `just init`), then simply compile and run:
```bash
cd hagrid
rustup override set nightly-2020-06-01
cp Rocket.toml.dist Rocket.toml
cargo run
```
@@ -53,17 +60,31 @@ will be statically built, and can be copied anywhere. You will also need to
adjust `Rocket.toml` accordingly. Hagrid uses `sendmail` for mailing, so you
also need a working local mailer setup.
Reverse Proxy
-------------
# Development Dependencies
Hagrid is designed to defer lookups to reverse proxy server like Nginx
and Apache. Lookups via `/vks/v1/by-finingerprint`, `/vks/v1/by-keyid`, and
`/vks/v1/by-email` can be handled by a robust and performant HTTP server. A
sample configuration for nginx is part of the repository (`nginx.conf`,
`hagrid-routes.conf`).
List of dependencies which are required or could be helpful for contribution
to the project.
| Category | Type | Name | Version | Verified Version | Notes |
|:------------------------:|:-----------:|:------------------------------------------:|:----------:|:----------------:|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Shell | Obligatory | [Zsh](https://zsh.sourceforge.io/) | \>= 5.9 | 5.9 | Required for [translated templates generation](./make-translated-templates) script. |
| VCS/SCM | Obligatory | [Git](https://git-scm.com/) | \>= 2.47.3 | 2.47.3 | Obviously, if going to get this repository you'll have the `git` CLI as dependency. But it also used in `just upgrade-rust` recipe to automate Rust upgrades. |
| Shell | Preferrable | [Bash](https://www.gnu.org/software/bash/) | \>= 5.2.15 | 5.2.15 | Required for scripts embedded into [`justfile`](./justfile). If you don't want to use [`just`](https://just.systems/) you probably don't need this shell. |
| Command Runner | Preferrable | [`just`](https://just.systems/) | \>= 1.42.4 | 1.40.0 | All commands from [`justfile`](./justfile) could be run without [`just`](https://just.systems/), but it makes development more convenient. |
| SQlite Prompt | Preferrable | [`sqlite3`](https://sqlite.org/cli.html) | \>= 3.40.1 | 3.40.1 | Used by [`just db`](./justfile) recipe to open interactive prompt to SQlite database of the project. |
| Command Line HTTP client | Preferrable | [`curl`](https://curl.se/) | \>= 8.14.1 | 8.14.1 | Used by `just _rust-stable-version` recipe to determine current stable version of Rust. Indirectly, used by `just upgrade-rust` depends on `curl` through `_rust-stable-version` recipe. |
| Text stream editor | Preferrable | [`sed`](https://www.gnu.org/software/sed/) | \>= 4.9 | 4.9 | Similar to `curl`, Used by `just _rust-stable-version` recipe to determine current stable version of Rust. Indirectly, used by `just upgrade-rust` depends on `curl` through `_rust-stable-version` recipe. |
| TOML Query | Preferrable | [tomlq](https://crates.io/crates/tomlq) | \>= 0.2.2 | 0.2.2 | Similar to `curl`, Used by `just _rust-stable-version` recipe to determine current stable version of Rust. Indirectly, used by `just upgrade-rust` depends on `curl` through `_rust-stable-version` recipe. |
Community
---------
We're in `#hagrid` on Freenode IRC, also reachable via Matrix as
`#hagrid:stratum0.org`.
We're in `#hagrid` on OFTC IRC, also reachable via Matrix as `#hagrid:stratum0.org`.
# Contribution
## Housekeeping
### Rust version upgrade
Take a look at `just upgrade-rust` recipe.
It bumps used version of Rust to the current stable version
(as [declared by manifest](https://static.rust-lang.org/dist/channel-rust-stable.toml)).

View File

@@ -2,7 +2,7 @@
address = "0.0.0.0"
port = 8080
[development]
[debug]
base-URI = "http://localhost:8080"
from = "noreply@localhost"
x-accel-redirect = false
@@ -36,7 +36,7 @@ maintenance_file = "maintenance"
enable_prometheus = false
email_template_dir = "email-templates"
[production]
[release]
base-URI = "https://keys.openpgp.org"
base-URI-Onion = "https://keys.openpgp.org"
from = "noreply@keys.openpgp.org"

View File

@@ -1,9 +1,6 @@
extern crate vergen;
use vergen::{generate_cargo_keys, ConstantsFlags};
use vergen::{ConstantsFlags, generate_cargo_keys};
fn main() {
// Generate the 'cargo:' key output
generate_cargo_keys(ConstantsFlags::all())
.expect("Unable to generate the cargo keys!");
generate_cargo_keys(ConstantsFlags::all()).expect("Unable to generate the cargo keys!");
}

3
clippy.toml Normal file
View File

@@ -0,0 +1,3 @@
msrv = "1.86"
too-many-arguments-threshold = 10

View File

@@ -1,70 +0,0 @@
#!/usr/bin/env zsh
set -e
[[ $# == 4 || $# == 5 ]] || { echo "Usage: $0 keys-internal-dir keys-external-dir encryption-key backup-dir [date]" >&2; exit 1; }
local keys_internal_dir=$1
local keys_external_dir=$2
local encryption_key=$3
local backup_dir=$4
# backupdate in format YYYY-MM-DD
local backupdate=$5
[[ -d $keys_internal_dir ]] || { echo "Missing dir $keys_internal_dir" >&2; exit 1; }
[[ -d $keys_internal_dir/log ]] || { echo "Missing dir $keys_internal_dir/log" >&2; exit 1; }
[[ -d $keys_external_dir ]] || { echo "Missing dir $keys_external_dir" >&2; exit 1; }
[[ -d $keys_external_dir/pub ]] || { echo "Missing dir $keys_external_dir/pub" >&2; exit 1; }
[[ -f $encryption_key ]] || { echo "Missing file $encryption_key" >&2; exit 1; }
[[ -d $backup_dir ]] || { echo "Missing dir $backup_dir" >&2; exit 1; }
if [[ -z $backupdate ]]; then
# for EPOCHSECONDS
zmodload zsh/datetime
backupdate="$(date --date=@$(( EPOCHSECONDS - 24*60*60 )) +'%Y-%m-%d')"
fi
local log_file="$keys_internal_dir/log/$backupdate"
[[ -f $log_file ]] || { echo "Missing dir $log_file" >&2; exit 1; }
local tempdir=$(mktemp -d)
trap "rm -rf ${(q)tempdir}" EXIT
local keylist_file=$tempdir/keylist
integer count=0
cat $log_file | cut -d' ' -f2 | sort -u | while read -r fp; do
key_file=${fp[1,2]}/${fp[3,4]}/${fp[5,$]}
[[ -f $keys_external_dir/pub/$key_file ]] || { echo "Missing file $key_file" >&2; exit 1; }
echo -E - $key_file
count+=1
done > $keylist_file
local backup_file_unencrypted=$tempdir/$backupdate.tar.gz
local backup_file_encrypted=$tempdir/$backupdate.tar.gz.pgp
tar \
--create \
--gzip \
--file $backup_file_unencrypted \
--verbatim-files-from \
--directory $keys_external_dir/pub \
--files-from $keylist_file
GNUPGHOME=$tempdir gpg \
--quiet \
--no-keyring \
--compress-level 0 \
--recipient-file $encryption_key \
--output $backup_file_encrypted \
--encrypt $backup_file_unencrypted
backup_file=$backup_dir/$backupdate.tar.gz.pgp
mv $backup_file_encrypted $backup_file
sha256sum="$(cd $backup_dir; sha256sum $backupdate.tar.gz.pgp)"
echo $sha256sum >> $backup_dir/SHA256SUM
echo "finished backup for $backupdate, total keys $count"
ls -l $backup_file
echo $sha256sum

View File

@@ -1,13 +0,0 @@
[Unit]
Description=Hagrid Verifying Keyserver
After=network.target
[Service]
ExecStart=/opt/hagrid/target/release/hagrid /opt/hagrid/dist -D %i -F hagrid@%i
WorkingDirectory=/opt/hagrid
User=hagrid
Group=hagrid
StandardOutput=syslog
[Install]
WantedBy=multi-user.target

View File

@@ -2,27 +2,31 @@
name = "hagrid-database"
version = "0.1.0"
authors = ["Kai Michaelis <kai@sequoia-pgp.org>"]
edition = "2024"
[dependencies]
anyhow = "1"
sequoia-openpgp = { version = "1.3", default-features = false, features = ["crypto-nettle"] }
multipart = "0"
log = "0"
rand = "0.6"
serde = { version = "1.0", features = ["derive"] }
serde_derive = "1.0"
serde_json = "1.0"
time = "0.1"
tempfile = "3.0"
url = "1.6"
hex = "0.3"
base64 = "0.10"
pathdiff = "0.1"
idna = "0.1"
fs2 = "0.4"
walkdir = "2.2"
chrono = "0.4"
zbase32 = "0.1.2"
anyhow = { workspace = true }
sequoia-openpgp = { workspace = true, features = ["crypto-openssl"] }
log = { workspace = true }
rand = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_derive = { workspace = true }
serde_json = { workspace = true }
time = { workspace = true }
tempfile = { workspace = true }
url = { workspace = true }
hex = { workspace = true }
base64 = { workspace = true }
pathdiff = { workspace = true }
idna = { workspace = true }
fs2 = { workspace = true }
walkdir = { workspace = true }
chrono = { workspace = true }
zbase32 = { workspace = true }
r2d2 = { workspace = true }
r2d2_sqlite = { workspace = true }
rusqlite = { workspace = true, features = ["trace"] }
self_cell = { workspace = true }
[lib]
name = "hagrid_database"

View File

@@ -1,939 +0,0 @@
use std::collections::HashMap;
use std::convert::TryFrom;
use std::fs::{OpenOptions, File, create_dir_all, read_link, remove_file, rename, set_permissions, Permissions};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::os::unix::fs::PermissionsExt;
use tempfile;
use url::form_urlencoded;
use pathdiff::diff_paths;
use std::time::SystemTime;
use {Database, Query};
use types::{Email, Fingerprint, KeyID};
use sync::FlockMutexGuard;
use Result;
use wkd;
use tempfile::NamedTempFile;
use openpgp::Cert;
use openpgp_utils::POLICY;
pub struct Filesystem {
tmp_dir: PathBuf,
keys_internal_dir: PathBuf,
keys_external_dir: PathBuf,
keys_dir_full: PathBuf,
keys_dir_quarantined: PathBuf,
keys_dir_published: PathBuf,
keys_dir_published_wkd: PathBuf,
keys_dir_log: PathBuf,
links_dir_by_fingerprint: PathBuf,
links_dir_by_keyid: PathBuf,
links_dir_wkd_by_email: PathBuf,
links_dir_by_email: PathBuf,
dry_run: bool,
}
/// Returns the given path, ensuring that the parent directory exists.
///
/// Use this on paths returned by .path_to_* before creating the
/// object.
fn ensure_parent(path: &Path) -> Result<&Path> {
let parent = path.parent().unwrap();
create_dir_all(parent)?;
Ok(path)
}
impl Filesystem {
pub fn new_from_base(base_dir: impl Into<PathBuf>) -> Result<Self> {
let base_dir: PathBuf = base_dir.into();
let keys_dir = base_dir.join("keys");
let tmp_dir = base_dir.join("tmp");
Self::new(&keys_dir, &keys_dir, tmp_dir)
}
pub fn new(
keys_internal_dir: impl Into<PathBuf>,
keys_external_dir: impl Into<PathBuf>,
tmp_dir: impl Into<PathBuf>,
) -> Result<Self> {
Self::new_internal(keys_internal_dir, keys_external_dir, tmp_dir, false)
}
pub fn new_internal(
keys_internal_dir: impl Into<PathBuf>,
keys_external_dir: impl Into<PathBuf>,
tmp_dir: impl Into<PathBuf>,
dry_run: bool,
) -> Result<Self> {
let tmp_dir = tmp_dir.into();
create_dir_all(&tmp_dir)?;
let keys_internal_dir: PathBuf = keys_internal_dir.into();
let keys_external_dir: PathBuf = keys_external_dir.into();
let keys_dir_full = keys_internal_dir.join("full");
let keys_dir_quarantined = keys_internal_dir.join("quarantined");
let keys_dir_log = keys_internal_dir.join("log");
let keys_dir_published = keys_external_dir.join("pub");
let keys_dir_published_wkd = keys_external_dir.join("wkd");
create_dir_all(&keys_dir_full)?;
create_dir_all(&keys_dir_quarantined)?;
create_dir_all(&keys_dir_published)?;
create_dir_all(&keys_dir_published_wkd)?;
create_dir_all(&keys_dir_log)?;
let links_dir = keys_external_dir.join("links");
let links_dir_by_keyid = links_dir.join("by-keyid");
let links_dir_by_fingerprint = links_dir.join("by-fpr");
let links_dir_by_email = links_dir.join("by-email");
let links_dir_wkd_by_email = links_dir.join("wkd");
create_dir_all(&links_dir_by_keyid)?;
create_dir_all(&links_dir_by_fingerprint)?;
create_dir_all(&links_dir_by_email)?;
create_dir_all(&links_dir_wkd_by_email)?;
info!("Opened filesystem database.");
info!("keys_internal_dir: '{}'", keys_internal_dir.display());
info!("keys_external_dir: '{}'", keys_external_dir.display());
info!("tmp_dir: '{}'", tmp_dir.display());
Ok(Filesystem {
keys_internal_dir,
keys_external_dir,
tmp_dir,
keys_dir_full,
keys_dir_published,
keys_dir_published_wkd,
keys_dir_quarantined,
keys_dir_log,
links_dir_by_keyid,
links_dir_by_fingerprint,
links_dir_by_email,
links_dir_wkd_by_email,
dry_run,
})
}
/// Returns the path to the given Fingerprint.
fn fingerprint_to_path_full(&self, fingerprint: &Fingerprint) -> PathBuf {
let hex = fingerprint.to_string();
self.keys_dir_full.join(path_split(&hex))
}
/// Returns the path to the given Fingerprint.
fn fingerprint_to_path_quarantined(&self, fingerprint: &Fingerprint) -> PathBuf {
let hex = fingerprint.to_string();
self.keys_dir_quarantined.join(&hex)
}
/// Returns the path to the given Fingerprint.
fn fingerprint_to_path_published(&self, fingerprint: &Fingerprint) -> PathBuf {
let hex = fingerprint.to_string();
self.keys_dir_published.join(path_split(&hex))
}
/// Returns the path to the given Fingerprint.
fn fingerprint_to_path_published_wkd(&self, fingerprint: &Fingerprint) -> PathBuf {
let hex = fingerprint.to_string();
self.keys_dir_published_wkd.join(path_split(&hex))
}
/// Returns the path to the given KeyID.
fn link_by_keyid(&self, keyid: &KeyID) -> PathBuf {
let hex = keyid.to_string();
self.links_dir_by_keyid.join(path_split(&hex))
}
/// Returns the path to the given Fingerprint.
fn link_by_fingerprint(&self, fingerprint: &Fingerprint) -> PathBuf {
let hex = fingerprint.to_string();
self.links_dir_by_fingerprint.join(path_split(&hex))
}
/// Returns the path to the given Email.
fn link_by_email(&self, email: &Email) -> PathBuf {
let email = form_urlencoded::byte_serialize(email.as_str().as_bytes())
.collect::<String>();
self.links_dir_by_email.join(path_split(&email))
}
/// Returns the WKD path to the given Email.
fn link_wkd_by_email(&self, email: &Email) -> PathBuf {
let (encoded_local_part, domain) = wkd::encode_wkd(email.as_str()).unwrap();
let encoded_domain = form_urlencoded::byte_serialize(domain.as_bytes())
.collect::<PathBuf>();
[
&self.links_dir_wkd_by_email,
&encoded_domain,
&path_split(&encoded_local_part)
].iter().collect()
}
fn read_from_path(&self, path: &Path, allow_internal: bool) -> Option<String> {
use std::fs;
if !path.starts_with(&self.keys_external_dir) &&
!(allow_internal && path.starts_with(&self.keys_internal_dir)) {
panic!("Attempted to access file outside expected dirs!");
}
if path.exists() {
fs::read_to_string(path).ok()
} else {
None
}
}
fn read_from_path_bytes(&self, path: &Path, allow_internal: bool) -> Option<Vec<u8>> {
use std::fs;
if !path.starts_with(&self.keys_external_dir) &&
!(allow_internal && path.starts_with(&self.keys_internal_dir)) {
panic!("Attempted to access file outside expected dirs!");
}
if path.exists() {
fs::read(path).ok()
} else {
None
}
}
/// Returns the Fingerprint the given path is pointing to.
pub fn path_to_fingerprint(path: &Path) -> Option<Fingerprint> {
use std::str::FromStr;
let merged = path_merge(path);
Fingerprint::from_str(&merged).ok()
}
/// Returns the KeyID the given path is pointing to.
fn path_to_keyid(path: &Path) -> Option<KeyID> {
use std::str::FromStr;
let merged = path_merge(path);
KeyID::from_str(&merged).ok()
}
/// Returns the Email the given path is pointing to.
fn path_to_email(path: &Path) -> Option<Email> {
use std::str::FromStr;
let merged = path_merge(path);
let decoded = form_urlencoded::parse(merged.as_bytes()).next()?.0;
Email::from_str(&decoded).ok()
}
/// Returns the backing primary key fingerprint for any key path.
pub fn path_to_primary(path: &Path) -> Option<Fingerprint> {
use std::fs;
let typ = fs::symlink_metadata(&path).ok()?.file_type();
if typ.is_symlink() {
let path = read_link(path).ok()?;
Filesystem::path_to_fingerprint(&path)
} else {
Filesystem::path_to_fingerprint(path)
}
}
fn link_email_vks(&self, email: &Email, fpr: &Fingerprint) -> Result<()> {
let path = self.fingerprint_to_path_published(fpr);
let link = self.link_by_email(&email);
let target = diff_paths(&path, link.parent().unwrap()).unwrap();
if link == target {
return Ok(());
}
symlink(&target, ensure_parent(&link)?)
}
fn link_email_wkd(&self, email: &Email, fpr: &Fingerprint) -> Result<()> {
let path = self.fingerprint_to_path_published_wkd(fpr);
let link = self.link_wkd_by_email(&email);
let target = diff_paths(&path, link.parent().unwrap()).unwrap();
if link == target {
return Ok(());
}
symlink(&target, ensure_parent(&link)?)
}
fn unlink_email_vks(&self, email: &Email, fpr: &Fingerprint) -> Result<()> {
let link = self.link_by_email(&email);
let expected = diff_paths(
&self.fingerprint_to_path_published(fpr),
link.parent().unwrap()
).unwrap();
symlink_unlink_with_check(&link, &expected)
}
fn unlink_email_wkd(&self, email: &Email, fpr: &Fingerprint) -> Result<()> {
let link = self.link_wkd_by_email(&email);
let expected = diff_paths(
&self.fingerprint_to_path_published_wkd(fpr),
link.parent().unwrap()
).unwrap();
symlink_unlink_with_check(&link, &expected)
}
fn open_logfile(&self, file_name: &str) -> Result<File> {
let file_path = self.keys_dir_log.join(file_name);
Ok(OpenOptions::new()
.create(true)
.append(true)
.open(file_path)?)
}
fn perform_checks(
&self,
checks_dir: &Path,
tpks: &mut HashMap<Fingerprint, Cert>,
check: impl Fn(&Path, &Cert, &Fingerprint) -> Result<()>,
) -> Result<()> {
use walkdir::WalkDir;
use std::fs;
for entry in WalkDir::new(checks_dir) {
let entry = entry?;
let path = entry.path();
let typ = fs::symlink_metadata(&path)?.file_type();
if typ.is_dir() {
continue;
}
// Compute the corresponding primary fingerprint just
// by looking at the paths.
let primary_fp = Filesystem::path_to_primary(path)
.ok_or_else(
|| format_err!("Malformed path: {:?}",
path.read_link().unwrap()))?;
// Load into cache.
if ! tpks.contains_key(&primary_fp) {
tpks.insert(
primary_fp.clone(),
self.lookup(&Query::ByFingerprint(primary_fp.clone()))
?.ok_or_else(
|| format_err!("No Cert with fingerprint {:?}",
primary_fp))?);
}
let tpk = tpks.get(&primary_fp)
.ok_or_else(
|| format_err!("Broken symlink {:?}: No such Key {}",
path, primary_fp))?;
check(&path, &tpk, &primary_fp)?;
}
Ok(())
}
}
// Like `symlink`, but instead of failing if `symlink_name` already
// exists, atomically update `symlink_name` to have `symlink_content`.
fn symlink(symlink_content: &Path, symlink_name: &Path) -> Result<()> {
use std::os::unix::fs::{symlink};
let symlink_dir = ensure_parent(symlink_name)?.parent().unwrap();
let tmp_dir = tempfile::Builder::new()
.prefix("link")
.rand_bytes(16)
.tempdir_in(symlink_dir)?;
let symlink_name_tmp = tmp_dir.path().join("link");
symlink(&symlink_content, &symlink_name_tmp)?;
rename(&symlink_name_tmp, &symlink_name)?;
Ok(())
}
fn symlink_unlink_with_check(link: &Path, expected: &Path) -> Result<()> {
if let Ok(target) = read_link(&link) {
if target == expected {
remove_file(link)?;
}
}
Ok(())
}
impl Database for Filesystem {
type MutexGuard = FlockMutexGuard;
fn lock(&self) -> Result<Self::MutexGuard> {
FlockMutexGuard::lock(&self.keys_internal_dir)
}
fn write_to_temp(&self, content: &[u8]) -> Result<NamedTempFile> {
let mut tempfile = tempfile::Builder::new()
.prefix("key")
.rand_bytes(16)
.tempfile_in(&self.tmp_dir)?;
tempfile.write_all(content).unwrap();
Ok(tempfile)
}
fn write_log_append(&self, filename: &str, fpr_primary: &Fingerprint) -> Result<()> {
let timestamp = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs();
let fingerprint_line = format!("{:010} {}\n", timestamp, fpr_primary.to_string());
self.open_logfile(filename)?
.write_all(fingerprint_line.as_bytes())?;
Ok(())
}
fn move_tmp_to_full(&self, file: NamedTempFile, fpr: &Fingerprint) -> Result<()> {
if self.dry_run {
return Ok(());
}
set_permissions(file.path(), Permissions::from_mode(0o640))?;
let target = self.fingerprint_to_path_full(fpr);
file.persist(ensure_parent(&target)?)?;
Ok(())
}
fn move_tmp_to_published(&self, file: NamedTempFile, fpr: &Fingerprint) -> Result<()> {
if self.dry_run {
return Ok(());
}
set_permissions(file.path(), Permissions::from_mode(0o644))?;
let target = self.fingerprint_to_path_published(fpr);
file.persist(ensure_parent(&target)?)?;
Ok(())
}
fn move_tmp_to_published_wkd(&self, file: Option<NamedTempFile>, fpr: &Fingerprint) -> Result<()> {
if self.dry_run {
return Ok(());
}
let target = self.fingerprint_to_path_published_wkd(fpr);
if let Some(file) = file {
set_permissions(file.path(), Permissions::from_mode(0o644))?;
file.persist(ensure_parent(&target)?)?;
} else if target.exists() {
remove_file(target)?;
}
Ok(())
}
fn write_to_quarantine(&self, fpr: &Fingerprint, content: &[u8]) -> Result<()> {
let mut tempfile = tempfile::Builder::new()
.prefix("key")
.rand_bytes(16)
.tempfile_in(&self.tmp_dir)?;
tempfile.write_all(content).unwrap();
let target = self.fingerprint_to_path_quarantined(fpr);
tempfile.persist(ensure_parent(&target)?)?;
Ok(())
}
fn check_link_fpr(&self, fpr: &Fingerprint, fpr_target: &Fingerprint) -> Result<Option<Fingerprint>> {
let link_keyid = self.link_by_keyid(&fpr.into());
let link_fpr = self.link_by_fingerprint(&fpr);
let path_published = self.fingerprint_to_path_published(fpr_target);
if let Ok(link_fpr_target) = link_fpr.canonicalize() {
if !link_fpr_target.ends_with(&path_published) {
info!("Fingerprint points to different key for {} (expected {:?} to be suffix of {:?})",
fpr, &path_published, &link_fpr_target);
Err(anyhow!(format!("Fingerprint collision for key {}", fpr)))?;
}
}
if let Ok(link_keyid_target) = link_keyid.canonicalize() {
if !link_keyid_target.ends_with(&path_published) {
info!("KeyID points to different key for {} (expected {:?} to be suffix of {:?})",
fpr, &path_published, &link_keyid_target);
Err(anyhow!(format!("KeyID collision for key {}", fpr)))?;
}
}
if !link_fpr.exists() || !link_keyid.exists() {
Ok(Some(fpr.clone()))
} else {
Ok(None)
}
}
fn lookup_primary_fingerprint(&self, term: &Query) -> Option<Fingerprint> {
use super::Query::*;
let path = match term {
ByFingerprint(ref fp) => self.link_by_fingerprint(fp),
ByKeyID(ref keyid) => self.link_by_keyid(keyid),
ByEmail(ref email) => self.link_by_email(email),
_ => return None
};
path.read_link()
.ok()
.and_then(|link_path| Filesystem::path_to_fingerprint(&link_path))
}
/// Gets the path to the underlying file, if any.
fn lookup_path(&self, term: &Query) -> Option<PathBuf> {
use super::Query::*;
let path = match term {
ByFingerprint(ref fp) => self.link_by_fingerprint(fp),
ByKeyID(ref keyid) => self.link_by_keyid(keyid),
ByEmail(ref email) => self.link_by_email(email),
_ => return None
};
if path.exists() {
let x = diff_paths(&path, &self.keys_external_dir).expect("related paths");
Some(x)
} else {
None
}
}
fn link_email(&self, email: &Email, fpr: &Fingerprint) -> Result<()> {
if self.dry_run {
return Ok(());
}
self.link_email_vks(email, fpr)?;
self.link_email_wkd(email, fpr)?;
Ok(())
}
fn unlink_email(&self, email: &Email, fpr: &Fingerprint) -> Result<()> {
self.unlink_email_vks(email, fpr)?;
self.unlink_email_wkd(email, fpr)?;
Ok(())
}
fn link_fpr(&self, from: &Fingerprint, primary_fpr: &Fingerprint) -> Result<()> {
if self.dry_run {
return Ok(());
}
let link_fpr = self.link_by_fingerprint(from);
let link_keyid = self.link_by_keyid(&from.into());
let target = diff_paths(&self.fingerprint_to_path_published(primary_fpr),
link_fpr.parent().unwrap()).unwrap();
symlink(&target, ensure_parent(&link_fpr)?)?;
symlink(&target, ensure_parent(&link_keyid)?)
}
fn unlink_fpr(&self, from: &Fingerprint, primary_fpr: &Fingerprint) -> Result<()> {
let link_fpr = self.link_by_fingerprint(from);
let link_keyid = self.link_by_keyid(&from.into());
let expected = diff_paths(&self.fingerprint_to_path_published(primary_fpr),
link_fpr.parent().unwrap()).unwrap();
match read_link(&link_fpr) {
Ok(target) => {
if target == expected {
remove_file(&link_fpr)?;
}
}
Err(_) => {}
}
match read_link(&link_keyid) {
Ok(target) => {
if target == expected {
remove_file(link_keyid)?;
}
}
Err(_) => {}
}
Ok(())
}
// XXX: slow
fn by_fpr_full(&self, fpr: &Fingerprint) -> Option<String> {
let path = self.fingerprint_to_path_full(fpr);
self.read_from_path(&path, true)
}
// XXX: slow
fn by_primary_fpr(&self, fpr: &Fingerprint) -> Option<String> {
let path = self.fingerprint_to_path_published(fpr);
self.read_from_path(&path, false)
}
// XXX: slow
fn by_fpr(&self, fpr: &Fingerprint) -> Option<String> {
let path = self.link_by_fingerprint(fpr);
self.read_from_path(&path, false)
}
// XXX: slow
fn by_email(&self, email: &Email) -> Option<String> {
let path = self.link_by_email(&email);
self.read_from_path(&path, false)
}
// XXX: slow
fn by_email_wkd(&self, email: &Email) -> Option<Vec<u8>> {
let path = self.link_wkd_by_email(&email);
self.read_from_path_bytes(&path, false)
}
// XXX: slow
fn by_kid(&self, kid: &KeyID) -> Option<String> {
let path = self.link_by_keyid(kid);
self.read_from_path(&path, false)
}
/// Checks the database for consistency.
///
/// Note that this operation may take a long time, and is
/// generally only useful for testing.
fn check_consistency(&self) -> Result<()> {
// A cache of all Certs, for quick lookups.
let mut tpks = HashMap::new();
self.perform_checks(&self.keys_dir_published, &mut tpks,
|path, _, primary_fp| {
// The KeyID corresponding with this path.
let fp = Filesystem::path_to_fingerprint(&path)
.ok_or_else(|| format_err!("Malformed path: {:?}", path))?;
if fp != *primary_fp {
return Err(format_err!(
"{:?} points to the wrong Cert, expected {} \
but found {}",
path, fp, primary_fp));
}
Ok(())
}
)?;
self.perform_checks(&self.keys_dir_published, &mut tpks,
|_, tpk, primary_fp| {
// check that certificate exists in published wkd path
let path_wkd = self.fingerprint_to_path_published_wkd(&primary_fp);
let should_wkd_exist = tpk.userids().next().is_some();
if should_wkd_exist && !path_wkd.exists() {
return Err(format_err!("Missing wkd for fp {}", primary_fp));
};
if !should_wkd_exist && path_wkd.exists() {
return Err(format_err!("Incorrectly present wkd for fp {}", primary_fp));
};
Ok(())
}
)?;
// check that all subkeys are linked
self.perform_checks(&self.keys_dir_published, &mut tpks,
|_, tpk, primary_fp| {
let policy = &POLICY;
let fingerprints = tpk
.keys()
.with_policy(policy, None)
.for_certification()
.for_signing()
.map(|amalgamation| amalgamation.key().fingerprint())
.map(|fpr| Fingerprint::try_from(fpr))
.flatten();
for fpr in fingerprints {
if let Some(missing_fpr) = self.check_link_fpr(&fpr, &primary_fp)? {
return Err(format_err!(
"Missing link to key {} for sub {}", primary_fp, missing_fpr));
}
}
Ok(())
}
)?;
// check that all published uids are linked
self.perform_checks(&self.keys_dir_published, &mut tpks,
|_, tpk, primary_fp| {
let emails = tpk
.userids()
.map(|binding| binding.userid().clone())
.map(|userid| Email::try_from(&userid).unwrap());
for email in emails {
let email_path = self.link_by_email(&email);
if !email_path.exists() {
return Err(format_err!(
"Missing link to key {} for email {}", primary_fp, email));
}
let email_wkd_path = self.link_wkd_by_email(&email);
if !email_wkd_path.exists() {
return Err(format_err!(
"Missing wkd link to key {} for email {}", primary_fp, email));
}
}
Ok(())
}
)?;
self.perform_checks(&self.links_dir_by_fingerprint, &mut tpks,
|path, tpk, _| {
// The KeyID corresponding with this path.
let id = Filesystem::path_to_keyid(&path)
.ok_or_else(|| format_err!("Malformed path: {:?}", path))?;
let found = tpk.keys()
.map(|amalgamation| KeyID::try_from(amalgamation.key().fingerprint()).unwrap())
.any(|key_fp| key_fp == id);
if ! found {
return Err(format_err!(
"{:?} points to the wrong Cert, the Cert does not \
contain the (sub)key {}", path, id));
}
Ok(())
}
)?;
self.perform_checks(&self.links_dir_by_keyid, &mut tpks,
|path, tpk, _| {
// The KeyID corresponding with this path.
let id = Filesystem::path_to_keyid(&path)
.ok_or_else(|| format_err!("Malformed path: {:?}", path))?;
let found = tpk.keys()
.map(|amalgamation| KeyID::try_from(amalgamation.key().fingerprint()).unwrap())
.any(|key_fp| key_fp == id);
if ! found {
return Err(format_err!(
"{:?} points to the wrong Cert, the Cert does not \
contain the (sub)key {}", path, id));
}
Ok(())
}
)?;
self.perform_checks(&self.links_dir_by_email, &mut tpks,
|path, tpk, _| {
// The Email corresponding with this path.
let email = Filesystem::path_to_email(&path)
.ok_or_else(|| format_err!("Malformed path: {:?}", path))?;
let mut found = false;
for uidb in tpk.userids() {
if Email::try_from(uidb.userid()).unwrap() == email
{
found = true;
break;
}
}
if ! found {
return Err(format_err!(
"{:?} points to the wrong Cert, the Cert does not \
contain the email {}", path, email));
}
Ok(())
})?;
Ok(())
}
}
fn path_split(path: &str) -> PathBuf {
if path.len() > 4 {
[&path[..2], &path[2..4], &path[4..]].iter().collect()
} else {
path.into()
}
}
fn path_merge(path: &Path) -> String {
let comps = path.iter().rev().take(3).collect::<Vec<_>>().into_iter().rev();
let comps: Vec<_> = comps.map(|os| os.to_string_lossy()).collect();
comps.join("")
}
#[cfg(test)]
mod tests {
use super::*;
use test;
use openpgp::cert::CertBuilder;
use tempfile::TempDir;
#[test]
fn init() {
let tmpdir = TempDir::new().unwrap();
let _ = Filesystem::new_from_base(tmpdir.path()).unwrap();
}
fn open_db() -> (TempDir, Filesystem, PathBuf) {
let tmpdir = TempDir::new().unwrap();
let db = Filesystem::new_from_base(tmpdir.path()).unwrap();
let log_path = db.keys_dir_log.join(db.get_current_log_filename());
(tmpdir, db, log_path)
}
#[test]
fn new() {
let (_tmp_dir, db, _log_path) = open_db();
let k1 = CertBuilder::new().add_userid("a@invalid.example.org")
.generate().unwrap().0;
let k2 = CertBuilder::new().add_userid("b@invalid.example.org")
.generate().unwrap().0;
let k3 = CertBuilder::new().add_userid("c@invalid.example.org")
.generate().unwrap().0;
assert!(db.merge(k1).unwrap().into_tpk_status().email_status.len() > 0);
assert!(db.merge(k2.clone()).unwrap().into_tpk_status().email_status.len() > 0);
assert!(!db.merge(k2).unwrap().into_tpk_status().email_status.len() > 0);
assert!(db.merge(k3.clone()).unwrap().into_tpk_status().email_status.len() > 0);
assert!(!db.merge(k3.clone()).unwrap().into_tpk_status().email_status.len() > 0);
assert!(!db.merge(k3).unwrap().into_tpk_status().email_status.len() > 0);
}
#[test]
fn uid_verification() {
let (_tmp_dir, mut db, log_path) = open_db();
test::test_uid_verification(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn uid_deletion() {
let (_tmp_dir, mut db, log_path) = open_db();
test::test_uid_deletion(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn subkey_lookup() {
let (_tmp_dir, mut db, log_path) = open_db();
test::test_subkey_lookup(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn kid_lookup() {
let (_tmp_dir, mut db, log_path) = open_db();
test::test_kid_lookup(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn upload_revoked_tpk() {
let (_tmp_dir, mut db, log_path) = open_db();
test::test_upload_revoked_tpk(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn uid_revocation() {
let (_tmp_dir, mut db, log_path) = open_db();
test::test_uid_revocation(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn regenerate() {
let (_tmp_dir, mut db, log_path) = open_db();
test::test_regenerate(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn key_reupload() {
let (_tmp_dir, mut db, log_path) = open_db();
test::test_reupload(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn uid_replacement() {
let (_tmp_dir, mut db, log_path) = open_db();
test::test_uid_replacement(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn uid_unlinking() {
let (_tmp_dir, mut db, log_path) = open_db();
test::test_unlink_uid(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn same_email_1() {
let (_tmp_dir, mut db, log_path) = open_db();
test::test_same_email_1(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn same_email_2() {
let (_tmp_dir, mut db, log_path) = open_db();
test::test_same_email_2(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn same_email_3() {
let (_tmp_dir, mut db, log_path) = open_db();
test::test_same_email_3(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn same_email_4() {
let (_tmp_dir, mut db, log_path) = open_db();
test::test_same_email_4(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn no_selfsig() {
let (_tmp_dir, mut db, log_path) = open_db();
test::test_no_selfsig(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn bad_uids() {
let (_tmp_dir, mut db, log_path) = open_db();
test::test_bad_uids(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn reverse_fingerprint_to_path() {
let tmpdir = TempDir::new().unwrap();
let db = Filesystem::new_from_base(tmpdir.path()).unwrap();
let fp: Fingerprint =
"CBCD8F030588653EEDD7E2659B7DD433F254904A".parse().unwrap();
assert_eq!(Filesystem::path_to_fingerprint(&db.link_by_fingerprint(&fp)),
Some(fp.clone()));
db.check_consistency().expect("inconsistent database");
}
#[test]
fn attested_key_signatures() -> Result<()> {
let (_tmp_dir, mut db, log_path) = open_db();
test::attested_key_signatures(&mut db, &log_path)?;
db.check_consistency()?;
Ok(())
}
}

View File

@@ -1,56 +1,31 @@
#![recursion_limit = "1024"]
use std::convert::TryFrom;
use std::path::PathBuf;
use std::str::FromStr;
use openpgp::serialize::SerializeInto;
use sequoia_openpgp::{
Cert, packet::UserID, parse::Parse, serialize::SerializeInto, types::KeyFlags,
};
use chrono::prelude::Utc;
#[macro_use]
extern crate anyhow;
use anyhow::Result;
extern crate fs2;
extern crate idna;
#[macro_use]
extern crate log;
extern crate pathdiff;
extern crate rand;
extern crate serde;
extern crate serde_json;
extern crate tempfile;
extern crate time;
extern crate url;
extern crate hex;
extern crate walkdir;
extern crate chrono;
extern crate zbase32;
use tempfile::NamedTempFile;
extern crate sequoia_openpgp as openpgp;
use openpgp::{
Cert,
packet::UserID,
parse::Parse,
types::KeyFlags,
};
use anyhow::anyhow;
use log::{error, info};
pub mod types;
use types::{Email, Fingerprint, KeyID};
pub mod wkd;
pub mod sync;
pub mod wkd;
mod fs;
pub use self::fs::Filesystem as KeyDatabase;
mod sqlite;
pub use crate::sqlite::Sqlite;
mod stateful_tokens;
pub use stateful_tokens::StatefulTokens;
mod openpgp_utils;
use openpgp_utils::{tpk_filter_alive_emails, tpk_to_string, tpk_clean, is_status_revoked, POLICY};
use openpgp_utils::{POLICY, is_status_revoked, tpk_clean, tpk_filter_alive_emails, tpk_to_string};
#[cfg(test)]
mod test;
@@ -67,22 +42,18 @@ pub enum Query {
impl Query {
pub fn is_invalid(&self) -> bool {
match self {
Query::Invalid() => true,
Query::InvalidShort() => true,
_ => false,
}
matches!(self, Query::Invalid() | Query::InvalidShort())
}
}
impl FromStr for Query {
type Err = anyhow::Error;
fn from_str(term: &str) -> Result<Self> {
fn from_str(term: &str) -> anyhow::Result<Self> {
use self::Query::*;
let looks_like_short_key_id = !term.contains('@') &&
(term.starts_with("0x") && term.len() < 16 || term.len() == 8);
let looks_like_short_key_id =
!term.contains('@') && (term.starts_with("0x") && term.len() < 16 || term.len() == 8);
if looks_like_short_key_id {
Ok(InvalidShort())
} else if let Ok(fp) = Fingerprint::from_str(term) {
@@ -97,7 +68,7 @@ impl FromStr for Query {
}
}
#[derive(Debug,PartialEq,Eq,PartialOrd,Ord)]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum EmailAddressStatus {
Published,
NotPublished,
@@ -118,12 +89,20 @@ impl ImportResult {
ImportResult::Unchanged(status) => status,
}
}
pub fn as_tpk_status(&self) -> &TpkStatus {
match self {
ImportResult::New(status) => status,
ImportResult::Updated(status) => status,
ImportResult::Unchanged(status) => status,
}
}
}
#[derive(Debug,PartialEq)]
#[derive(Debug, PartialEq)]
pub struct TpkStatus {
pub is_revoked: bool,
pub email_status: Vec<(Email,EmailAddressStatus)>,
pub email_status: Vec<(Email, EmailAddressStatus)>,
pub unparsed_uids: usize,
}
@@ -132,23 +111,73 @@ pub enum RegenerateResult {
Unchanged,
}
pub trait Database: Sync + Send {
type MutexGuard;
pub trait DatabaseTransaction<'a> {
type TempCert;
fn commit(self) -> anyhow::Result<()>;
fn link_email(&self, email: &Email, fpr: &Fingerprint) -> anyhow::Result<()>;
fn unlink_email(&self, email: &Email, fpr: &Fingerprint) -> anyhow::Result<()>;
fn link_fpr(&self, from: &Fingerprint, to: &Fingerprint) -> anyhow::Result<()>;
fn unlink_fpr(&self, from: &Fingerprint, to: &Fingerprint) -> anyhow::Result<()>;
fn write_to_temp(&self, content: &[u8]) -> anyhow::Result<Self::TempCert>;
fn move_tmp_to_full(&self, content: Self::TempCert, fpr: &Fingerprint) -> anyhow::Result<()>;
fn move_tmp_to_published(
&self,
content: Self::TempCert,
fpr: &Fingerprint,
) -> anyhow::Result<()>;
fn move_tmp_to_published_wkd(
&self,
content: Option<Self::TempCert>,
fpr: &Fingerprint,
) -> anyhow::Result<()>;
fn write_to_quarantine(&self, fpr: &Fingerprint, content: &[u8]) -> anyhow::Result<()>;
}
pub trait Database<'a>: Sync + Send {
type Transaction: DatabaseTransaction<'a>;
/// Lock the DB for a complex update.
///
/// All basic write operations are atomic so we don't need to lock
/// read operations to ensure that we return something sane.
fn lock(&self) -> Result<Self::MutexGuard>;
fn transaction(&'a self) -> anyhow::Result<Self::Transaction>;
/// Queries the database using Fingerprint, KeyID, or
/// email-address, returning the primary fingerprint.
fn lookup_primary_fingerprint(&self, term: &Query) -> Option<Fingerprint>;
fn by_fpr(&self, fpr: &Fingerprint) -> Option<String>;
fn by_kid(&self, kid: &KeyID) -> Option<String>;
fn by_email(&self, email: &Email) -> Option<String>;
fn by_email_wkd(&self, email: &Email) -> Option<Vec<u8>>;
fn by_domain_and_hash_wkd(&self, domain: &str, hash: &str) -> Option<Vec<u8>>;
fn by_fpr_full(&self, fpr: &Fingerprint) -> Option<String>;
fn by_primary_fpr(&self, fpr: &Fingerprint) -> Option<String>;
fn get_last_log_entry(&self) -> anyhow::Result<Fingerprint>;
fn write_log_append(&self, filename: &str, fpr_primary: &Fingerprint) -> anyhow::Result<()>;
fn check_link_fpr(
&self,
fpr: &Fingerprint,
target: &Fingerprint,
) -> anyhow::Result<Option<Fingerprint>>;
fn check_consistency(&self) -> anyhow::Result<()>;
/// Queries the database using Fingerprint, KeyID, or
/// email-address.
fn lookup(&self, term: &Query) -> Result<Option<Cert>> {
fn lookup(&self, term: &Query) -> anyhow::Result<Option<Cert>> {
use self::Query::*;
let armored = match term {
ByFingerprint(ref fp) => self.by_fpr(fp),
ByKeyID(ref keyid) => self.by_kid(keyid),
ByEmail(ref email) => self.by_email(&email),
ByFingerprint(fp) => self.by_fpr(fp),
ByKeyID(keyid) => self.by_kid(keyid),
ByEmail(email) => self.by_email(email),
_ => None,
};
@@ -158,27 +187,6 @@ pub trait Database: Sync + Send {
}
}
/// Queries the database using Fingerprint, KeyID, or
/// email-address, returning the primary fingerprint.
fn lookup_primary_fingerprint(&self, term: &Query) -> Option<Fingerprint>;
/// Gets the path to the underlying file, if any.
fn lookup_path(&self, term: &Query) -> Option<PathBuf> {
let _ = term;
None
}
fn link_email(&self, email: &Email, fpr: &Fingerprint) -> Result<()>;
fn unlink_email(&self, email: &Email, fpr: &Fingerprint) -> Result<()>;
fn link_fpr(&self, from: &Fingerprint, to: &Fingerprint) -> Result<()>;
fn unlink_fpr(&self, from: &Fingerprint, to: &Fingerprint) -> Result<()>;
fn by_fpr(&self, fpr: &Fingerprint) -> Option<String>;
fn by_kid(&self, kid: &KeyID) -> Option<String>;
fn by_email(&self, email: &Email) -> Option<String>;
fn by_email_wkd(&self, email: &Email) -> Option<Vec<u8>>;
/// Complex operation that updates a Cert in the database.
///
/// 1. Merge new Cert with old, full Cert
@@ -191,17 +199,18 @@ pub trait Database: Sync + Send {
/// - abort if any problems come up!
/// 5. Move full and published temporary Cert to their location
/// 6. Update all symlinks
fn merge(&self, new_tpk: Cert) -> Result<ImportResult> {
fn merge(&'a self, new_tpk: Cert) -> anyhow::Result<ImportResult> {
let fpr_primary = Fingerprint::try_from(new_tpk.primary_key().fingerprint())?;
let _lock = self.lock()?;
let tx = self.transaction()?;
let known_uids: Vec<UserID> = new_tpk
.userids()
.map(|binding| binding.userid().clone())
.collect();
let full_tpk_old = self.by_fpr_full(&fpr_primary)
let full_tpk_old = self
.by_fpr_full(&fpr_primary)
.and_then(|bytes| Cert::from_bytes(bytes.as_bytes()).ok());
let is_update = full_tpk_old.is_some();
let (full_tpk_new, full_tpk_unchanged) = if let Some(full_tpk_old) = full_tpk_old {
@@ -214,9 +223,14 @@ pub trait Database: Sync + Send {
let is_revoked = is_status_revoked(full_tpk_new.revocation_status(&POLICY, None));
let is_ok = is_revoked ||
full_tpk_new.keys().subkeys().next().is_some() ||
full_tpk_new.userids().next().is_some();
let is_ok = is_revoked
|| full_tpk_new.keys().subkeys().next().is_some()
|| full_tpk_new.userids().next().is_some()
|| full_tpk_new
.primary_key()
.self_signatures()
.next()
.is_some();
if !is_ok {
// self.write_to_quarantine(&fpr_primary, &tpk_to_string(&full_tpk_new)?)?;
return Err(anyhow!("Not a well-formed key!"));
@@ -225,7 +239,10 @@ pub trait Database: Sync + Send {
let published_tpk_old = self
.by_fpr(&fpr_primary)
.and_then(|bytes| Cert::from_bytes(bytes.as_bytes()).ok());
let published_emails = published_tpk_old.as_ref().map(|cert| tpk_get_emails(cert)).unwrap_or_default();
let published_emails = published_tpk_old
.as_ref()
.map(tpk_get_emails)
.unwrap_or_default();
let unparsed_uids = full_tpk_new
.userids()
@@ -235,15 +252,17 @@ pub trait Database: Sync + Send {
let mut email_status: Vec<_> = full_tpk_new
.userids()
.map(|binding| {
.filter_map(|binding| {
if let Ok(email) = Email::try_from(binding.userid()) {
Some((binding, email))
} else {
None
}
})
.flatten()
.filter(|(binding, email)| known_uids.contains(binding.userid()) || published_emails.contains(email))
.filter(|(binding, _)| binding.self_signatures().next().is_some())
.filter(|(binding, email)| {
known_uids.contains(binding.userid()) || published_emails.contains(email)
})
.flat_map(|(binding, email)| {
if is_status_revoked(binding.revocation_status(&POLICY, None)) {
Some((email, EmailAddressStatus::Revoked))
@@ -261,7 +280,11 @@ pub trait Database: Sync + Send {
// Abort if no changes were made
if full_tpk_unchanged {
return Ok(ImportResult::Unchanged(TpkStatus { is_revoked, email_status, unparsed_uids }));
return Ok(ImportResult::Unchanged(TpkStatus {
is_revoked,
email_status,
unparsed_uids,
}));
}
let published_tpk_new = if is_revoked {
@@ -277,37 +300,37 @@ pub trait Database: Sync + Send {
.userids()
.filter(|binding| !is_status_revoked(binding.revocation_status(&POLICY, None)))
.map(|binding| binding.userid())
.map(|uid| Email::try_from(uid).ok())
.flatten()
.flat_map(Email::try_from)
.any(|unrevoked_email| &unrevoked_email == *email);
!has_unrevoked_userid
}).collect();
})
.collect();
let fingerprints = tpk_get_linkable_fprs(&published_tpk_new);
let fpr_checks = fingerprints
.iter()
.map(|fpr| self.check_link_fpr(&fpr, &fpr_primary))
.map(|fpr| self.check_link_fpr(fpr, &fpr_primary))
.collect::<Vec<_>>()
.into_iter()
.collect::<Result<Vec<_>>>();
.collect::<anyhow::Result<Vec<_>>>();
if fpr_checks.is_err() {
self.write_to_quarantine(&fpr_primary, &tpk_to_string(&full_tpk_new)?)?;
tx.write_to_quarantine(&fpr_primary, &tpk_to_string(&full_tpk_new)?)?;
}
let fpr_checks = fpr_checks?;
let fpr_not_linked = fpr_checks.into_iter().flatten();
let full_tpk_tmp = self.write_to_temp(&tpk_to_string(&full_tpk_new)?)?;
let full_tpk_tmp = tx.write_to_temp(&tpk_to_string(&full_tpk_new)?)?;
let published_tpk_clean = tpk_clean(&published_tpk_new)?;
let published_tpk_tmp = self.write_to_temp(&tpk_to_string(&published_tpk_clean)?)?;
let published_tpk_tmp = tx.write_to_temp(&tpk_to_string(&published_tpk_clean)?)?;
// these are very unlikely to fail. but if it happens,
// database consistency might be compromised!
self.move_tmp_to_full(full_tpk_tmp, &fpr_primary)?;
self.move_tmp_to_published(published_tpk_tmp, &fpr_primary)?;
self.regenerate_wkd(&fpr_primary, &published_tpk_clean)?;
tx.move_tmp_to_full(full_tpk_tmp, &fpr_primary)?;
tx.move_tmp_to_published(published_tpk_tmp, &fpr_primary)?;
self.regenerate_wkd(&tx, &fpr_primary, &published_tpk_clean)?;
let published_tpk_changed = published_tpk_old
.map(|tpk| tpk != published_tpk_clean)
@@ -317,29 +340,39 @@ pub trait Database: Sync + Send {
}
for fpr in fpr_not_linked {
if let Err(e) = self.link_fpr(&fpr, &fpr_primary) {
info!("Error ensuring symlink! {} {} {:?}",
&fpr, &fpr_primary, e);
if let Err(e) = tx.link_fpr(&fpr, &fpr_primary) {
info!("Error ensuring symlink! {} {} {:?}", &fpr, &fpr_primary, e);
}
}
for revoked_email in newly_revoked_emails {
if let Err(e) = self.unlink_email(&revoked_email, &fpr_primary) {
info!("Error ensuring symlink! {} {} {:?}",
&fpr_primary, &revoked_email, e);
if let Err(e) = tx.unlink_email(revoked_email, &fpr_primary) {
info!(
"Error ensuring symlink! {} {} {:?}",
&fpr_primary, &revoked_email, e
);
}
}
tx.commit()?;
if is_update {
Ok(ImportResult::Updated(TpkStatus { is_revoked, email_status, unparsed_uids }))
Ok(ImportResult::Updated(TpkStatus {
is_revoked,
email_status,
unparsed_uids,
}))
} else {
Ok(ImportResult::New(TpkStatus { is_revoked, email_status, unparsed_uids }))
Ok(ImportResult::New(TpkStatus {
is_revoked,
email_status,
unparsed_uids,
}))
}
}
fn update_write_log(&self, fpr_primary: &Fingerprint) {
let log_name = self.get_current_log_filename();
println!("{}", log_name);
if let Err(e) = self.write_log_append(&log_name, fpr_primary) {
error!("Error writing to log! {} {} {}", &log_name, &fpr_primary, e);
}
@@ -349,8 +382,13 @@ pub trait Database: Sync + Send {
Utc::now().format("%Y-%m-%d").to_string()
}
fn get_tpk_status(&self, fpr_primary: &Fingerprint, known_addresses: &[Email]) -> Result<TpkStatus> {
let tpk_full = self.by_fpr_full(&fpr_primary)
fn get_tpk_status(
&self,
fpr_primary: &Fingerprint,
known_addresses: &[Email],
) -> anyhow::Result<TpkStatus> {
let tpk_full = self
.by_fpr_full(fpr_primary)
.ok_or_else(|| anyhow!("Key not in database!"))
.and_then(|bytes| Cert::from_bytes(bytes.as_bytes()))?;
@@ -363,15 +401,18 @@ pub trait Database: Sync + Send {
.count();
let published_uids: Vec<UserID> = self
.by_fpr(&fpr_primary)
.by_fpr(fpr_primary)
.and_then(|bytes| Cert::from_bytes(bytes.as_bytes()).ok())
.map(|tpk| tpk.userids()
.map(|binding| binding.userid().clone())
.collect()
).unwrap_or_default();
.map(|tpk| {
tpk.userids()
.map(|binding| binding.userid().clone())
.collect()
})
.unwrap_or_default();
let mut email_status: Vec<_> = tpk_full
.userids()
.filter(|binding| binding.self_signatures().next().is_some())
.flat_map(|binding| {
let uid = binding.userid();
if let Ok(email) = Email::try_from(uid) {
@@ -394,7 +435,11 @@ pub trait Database: Sync + Send {
// the same address, we keep the first.
email_status.dedup_by(|(e1, _), (e2, _)| e1 == e2);
Ok(TpkStatus { is_revoked, email_status, unparsed_uids })
Ok(TpkStatus {
is_revoked,
email_status,
unparsed_uids,
})
}
/// Complex operation that publishes some user id for a Cert already in the database.
@@ -410,74 +455,88 @@ pub trait Database: Sync + Send {
/// - abort if any problems come up!
/// 5. Move full and published temporary Cert to their location
/// 6. Update all symlinks
fn set_email_published(&self, fpr_primary: &Fingerprint, email_new: &Email) -> Result<()> {
let _lock = self.lock()?;
fn set_email_published(
&'a self,
fpr_primary: &Fingerprint,
email_new: &Email,
) -> anyhow::Result<()> {
let tx = self.transaction()?;
self.nolock_unlink_email_if_other(fpr_primary, email_new)?;
self.unlink_email_if_other(&tx, fpr_primary, email_new)?;
let full_tpk = self.by_fpr_full(&fpr_primary)
let full_tpk = self
.by_fpr_full(fpr_primary)
.ok_or_else(|| anyhow!("Key not in database!"))
.and_then(|bytes| Cert::from_bytes(bytes.as_bytes()))?;
let published_uids_old: Vec<UserID> = self
.by_fpr(&fpr_primary)
.by_fpr(fpr_primary)
.and_then(|bytes| Cert::from_bytes(bytes.as_bytes()).ok())
.map(|tpk| tpk.userids()
.map(|binding| binding.userid().clone())
.collect()
).unwrap_or_default();
let published_emails_old: Vec<Email> = published_uids_old.iter()
.map(|uid| Email::try_from(uid).ok())
.flatten()
.map(|tpk| {
tpk.userids()
.map(|binding| binding.userid().clone())
.collect()
})
.unwrap_or_default();
let published_emails_old: Vec<Email> = published_uids_old
.iter()
.flat_map(Email::try_from)
.collect();
// println!("publishing: {:?}", &uid_new);
if published_emails_old.contains(&email_new) {
if published_emails_old.contains(email_new) {
// UserID already published - just stop
return Ok(());
}
let mut published_emails = published_emails_old.clone();
let mut published_emails = published_emails_old;
published_emails.push(email_new.clone());
let published_tpk_new = tpk_filter_alive_emails(&full_tpk, &published_emails);
if !published_tpk_new
.userids()
.map(|binding| Email::try_from(binding.userid()))
.flatten()
.any(|email| email == *email_new) {
return Err(anyhow!("Requested UserID not found!"));
.flat_map(|binding| Email::try_from(binding.userid()))
.any(|email| email == *email_new)
{
return Err(anyhow!("Requested UserID not found!"));
}
let published_tpk_clean = tpk_clean(&published_tpk_new)?;
let published_tpk_tmp = self.write_to_temp(&tpk_to_string(&published_tpk_clean)?)?;
let published_tpk_tmp = tx.write_to_temp(&tpk_to_string(&published_tpk_clean)?)?;
self.move_tmp_to_published(published_tpk_tmp, &fpr_primary)?;
self.regenerate_wkd(fpr_primary, &published_tpk_clean)?;
tx.move_tmp_to_published(published_tpk_tmp, fpr_primary)?;
self.regenerate_wkd(&tx, fpr_primary, &published_tpk_clean)?;
self.update_write_log(&fpr_primary);
self.update_write_log(fpr_primary);
if let Err(e) = self.link_email(&email_new, &fpr_primary) {
info!("Error ensuring email symlink! {} -> {} {:?}",
&email_new, &fpr_primary, e);
if let Err(e) = tx.link_email(email_new, fpr_primary) {
info!(
"Error ensuring email symlink! {} -> {} {:?}",
&email_new, &fpr_primary, e
);
}
tx.commit()?;
Ok(())
}
fn nolock_unlink_email_if_other(
fn unlink_email_if_other(
&self,
tx: &Self::Transaction,
fpr_primary: &Fingerprint,
unlink_email: &Email,
) -> Result<()> {
let current_link_fpr = self.lookup_primary_fingerprint(
&Query::ByEmail(unlink_email.clone()));
) -> anyhow::Result<()> {
let current_link_fpr =
self.lookup_primary_fingerprint(&Query::ByEmail(unlink_email.clone()));
if let Some(current_fpr) = current_link_fpr {
if current_fpr != *fpr_primary {
self.nolock_set_email_unpublished_filter(&current_fpr,
|uid| Email::try_from(uid).map(|email| email != *unlink_email)
.unwrap_or(false))?;
self.set_email_unpublished_filter(tx, &current_fpr, |uid| {
Email::try_from(uid)
.map(|email| email != *unlink_email)
.unwrap_or(false)
})?;
}
}
Ok(())
@@ -498,35 +557,25 @@ pub trait Database: Sync + Send {
/// 6. Update all symlinks
fn set_email_unpublished_filter(
&self,
tx: &Self::Transaction,
fpr_primary: &Fingerprint,
email_remove: impl Fn(&UserID) -> bool,
) -> Result<()> {
let _lock = self.lock()?;
self.nolock_set_email_unpublished_filter(fpr_primary, email_remove)
}
fn nolock_set_email_unpublished_filter(
&self,
fpr_primary: &Fingerprint,
email_remove: impl Fn(&UserID) -> bool,
) -> Result<()> {
let published_tpk_old = self.by_fpr(&fpr_primary)
) -> anyhow::Result<()> {
let published_tpk_old = self
.by_fpr(fpr_primary)
.ok_or_else(|| anyhow!("Key not in database!"))
.and_then(|bytes| Cert::from_bytes(bytes.as_bytes()))?;
let published_emails_old: Vec<Email> = published_tpk_old
.userids()
.map(|binding| Email::try_from(binding.userid()))
.flatten()
.flat_map(|binding| Email::try_from(binding.userid()))
.collect();
let published_tpk_new = published_tpk_old.clone().retain_userids(
|uid| email_remove(uid.userid()));
let published_tpk_new = published_tpk_old.retain_userids(|uid| email_remove(uid.userid()));
let published_emails_new: Vec<Email> = published_tpk_new
.userids()
.map(|binding| Email::try_from(binding.userid()))
.flatten()
.flat_map(|binding| Email::try_from(binding.userid()))
.collect();
let unpublished_emails = published_emails_old
@@ -534,17 +583,19 @@ pub trait Database: Sync + Send {
.filter(|email| !published_emails_new.contains(email));
let published_tpk_clean = tpk_clean(&published_tpk_new)?;
let published_tpk_tmp = self.write_to_temp(&tpk_to_string(&published_tpk_clean)?)?;
let published_tpk_tmp = tx.write_to_temp(&tpk_to_string(&published_tpk_clean)?)?;
self.move_tmp_to_published(published_tpk_tmp, &fpr_primary)?;
self.regenerate_wkd(fpr_primary, &published_tpk_clean)?;
tx.move_tmp_to_published(published_tpk_tmp, fpr_primary)?;
self.regenerate_wkd(tx, fpr_primary, &published_tpk_clean)?;
self.update_write_log(&fpr_primary);
self.update_write_log(fpr_primary);
for unpublished_email in unpublished_emails {
if let Err(e) = self.unlink_email(&unpublished_email, &fpr_primary) {
info!("Error deleting email symlink! {} -> {} {:?}",
&unpublished_email, &fpr_primary, e);
if let Err(e) = tx.unlink_email(unpublished_email, fpr_primary) {
info!(
"Error deleting email symlink! {} -> {} {:?}",
&unpublished_email, &fpr_primary, e
);
}
}
@@ -552,47 +603,50 @@ pub trait Database: Sync + Send {
}
fn set_email_unpublished(
&self,
&'a self,
fpr_primary: &Fingerprint,
email_remove: &Email,
) -> Result<()> {
self.set_email_unpublished_filter(fpr_primary, |uid|
) -> anyhow::Result<()> {
let tx = self.transaction().unwrap();
self.set_email_unpublished_filter(&tx, fpr_primary, |uid| {
Email::try_from(uid)
.map(|email| email != *email_remove)
.unwrap_or(false))
.unwrap_or(false)
})?;
tx.commit()?;
Ok(())
}
fn set_email_unpublished_all(
&self,
fpr_primary: &Fingerprint,
) -> Result<()> {
self.set_email_unpublished_filter(fpr_primary, |_| false)
fn set_email_unpublished_all(&'a self, fpr_primary: &Fingerprint) -> anyhow::Result<()> {
let tx = self.transaction().unwrap();
self.set_email_unpublished_filter(&tx, fpr_primary, |_| false)?;
tx.commit()?;
Ok(())
}
fn regenerate_links(
&self,
fpr_primary: &Fingerprint,
) -> Result<RegenerateResult> {
let tpk = self.by_primary_fpr(&fpr_primary)
fn regenerate_links(&'a self, fpr_primary: &Fingerprint) -> anyhow::Result<RegenerateResult> {
let tx = self.transaction().unwrap();
let tpk = self
.by_primary_fpr(fpr_primary)
.and_then(|bytes| Cert::from_bytes(bytes.as_bytes()).ok())
.ok_or_else(|| anyhow!("Key not in database!"))?;
let published_emails: Vec<Email> = tpk
.userids()
.map(|binding| Email::try_from(binding.userid()))
.flatten()
.flat_map(|binding| Email::try_from(binding.userid()))
.collect();
self.regenerate_wkd(fpr_primary, &tpk)?;
self.regenerate_wkd(&tx, fpr_primary, &tpk)?;
let fingerprints = tpk_get_linkable_fprs(&tpk);
let fpr_checks = fingerprints
.into_iter()
.map(|fpr| self.check_link_fpr(&fpr, &fpr_primary))
.map(|fpr| self.check_link_fpr(&fpr, fpr_primary))
.collect::<Vec<_>>()
.into_iter()
.collect::<Result<Vec<_>>>()?;
.collect::<anyhow::Result<Vec<_>>>()?;
let fpr_not_linked = fpr_checks.into_iter().flatten();
@@ -601,14 +655,16 @@ pub trait Database: Sync + Send {
for fpr in fpr_not_linked {
keys_linked += 1;
self.link_fpr(&fpr, &fpr_primary)?;
tx.link_fpr(&fpr, fpr_primary)?;
}
for email in published_emails {
emails_linked += 1;
self.link_email(&email, &fpr_primary)?;
tx.link_email(&email, fpr_primary)?;
}
tx.commit()?;
if keys_linked != 0 || emails_linked != 0 {
Ok(RegenerateResult::Updated)
} else {
@@ -618,59 +674,47 @@ pub trait Database: Sync + Send {
fn regenerate_wkd(
&self,
tx: &Self::Transaction,
fpr_primary: &Fingerprint,
published_tpk: &Cert
) -> Result<()> {
published_tpk: &Cert,
) -> anyhow::Result<()> {
let published_wkd_tpk_tmp = if published_tpk.userids().next().is_some() {
Some(self.write_to_temp(&published_tpk.to_vec()?)?)
Some(tx.write_to_temp(&published_tpk.export_to_vec()?)?)
} else {
None
};
self.move_tmp_to_published_wkd(published_wkd_tpk_tmp, fpr_primary)?;
tx.move_tmp_to_published_wkd(published_wkd_tpk_tmp, fpr_primary)?;
Ok(())
}
fn check_link_fpr(&self, fpr: &Fingerprint, target: &Fingerprint) -> Result<Option<Fingerprint>>;
fn by_fpr_full(&self, fpr: &Fingerprint) -> Option<String>;
fn by_primary_fpr(&self, fpr: &Fingerprint) -> Option<String>;
fn write_to_temp(&self, content: &[u8]) -> Result<NamedTempFile>;
fn move_tmp_to_full(&self, content: NamedTempFile, fpr: &Fingerprint) -> Result<()>;
fn move_tmp_to_published(&self, content: NamedTempFile, fpr: &Fingerprint) -> Result<()>;
fn move_tmp_to_published_wkd(&self, content: Option<NamedTempFile>, fpr: &Fingerprint) -> Result<()>;
fn write_to_quarantine(&self, fpr: &Fingerprint, content: &[u8]) -> Result<()>;
fn write_log_append(&self, filename: &str, fpr_primary: &Fingerprint) -> Result<()>;
fn check_consistency(&self) -> Result<()>;
}
fn tpk_get_emails(cert: &Cert) -> Vec<Email> {
cert
.userids()
.map(|binding| Email::try_from(binding.userid()))
.flatten()
cert.userids()
.flat_map(|binding| Email::try_from(binding.userid()))
.collect()
}
pub fn tpk_get_linkable_fprs(tpk: &Cert) -> Vec<Fingerprint> {
let ref signing_capable = KeyFlags::empty()
.set_signing()
.set_certification();
let ref fpr_primary = Fingerprint::try_from(tpk.fingerprint()).unwrap();
tpk
.keys()
.into_iter()
.flat_map(|bundle| {
Fingerprint::try_from(bundle.key().fingerprint())
.map(|fpr| (fpr, bundle.binding_signature(&POLICY, None).ok().and_then(|sig| sig.key_flags())))
let signing_capable = &KeyFlags::empty().set_signing().set_certification();
let fpr_primary = &Fingerprint::try_from(tpk.fingerprint()).unwrap();
tpk.keys()
.flat_map(|bundle| {
Fingerprint::try_from(bundle.key().fingerprint()).map(|fpr| {
(
fpr,
bundle
.binding_signature(&POLICY, None)
.ok()
.and_then(|sig| sig.key_flags()),
)
})
.filter(|(fpr, flags)| {
fpr == fpr_primary ||
flags.is_none() ||
!(signing_capable & flags.as_ref().unwrap()).is_empty()
})
.map(|(fpr,_)| fpr)
.collect()
})
.filter(|(fpr, flags)| {
fpr == fpr_primary
|| flags.is_none()
|| !(signing_capable & flags.as_ref().unwrap()).is_empty()
})
.map(|(fpr, _)| fpr)
.collect()
}

View File

@@ -1,15 +1,11 @@
use openpgp::Result;
use std::convert::TryFrom;
use openpgp::{
Cert,
use sequoia_openpgp::{
Cert, Result, cert::prelude::*, policy::StandardPolicy, serialize::SerializeInto as _,
types::RevocationStatus,
cert::prelude::*,
serialize::SerializeInto as _,
policy::StandardPolicy,
};
use Email;
use crate::Email;
pub const POLICY: StandardPolicy = StandardPolicy::new();
@@ -33,24 +29,42 @@ pub fn tpk_clean(tpk: &Cert) -> Result<Cert> {
// The primary key and related signatures.
let pk_bundle = tpk.primary_key().bundle();
acc.push(pk_bundle.key().clone().into());
for s in pk_bundle.self_signatures() { acc.push(s.clone().into()) }
for s in pk_bundle.self_revocations() { acc.push(s.clone().into()) }
for s in pk_bundle.other_revocations() { acc.push(s.clone().into()) }
for s in pk_bundle.self_signatures() {
acc.push(s.clone().into())
}
for s in pk_bundle.self_revocations() {
acc.push(s.clone().into())
}
for s in pk_bundle.other_revocations() {
acc.push(s.clone().into())
}
// The subkeys and related signatures.
for skb in tpk.keys().subkeys() {
acc.push(skb.key().clone().into());
for s in skb.self_signatures() { acc.push(s.clone().into()) }
for s in skb.self_revocations() { acc.push(s.clone().into()) }
for s in skb.other_revocations() { acc.push(s.clone().into()) }
for s in skb.self_signatures() {
acc.push(s.clone().into())
}
for s in skb.self_revocations() {
acc.push(s.clone().into())
}
for s in skb.other_revocations() {
acc.push(s.clone().into())
}
}
// The UserIDs.
for uidb in tpk.userids() {
acc.push(uidb.userid().clone().into());
for s in uidb.self_signatures() { acc.push(s.clone().into()) }
for s in uidb.self_revocations() { acc.push(s.clone().into()) }
for s in uidb.other_revocations() { acc.push(s.clone().into()) }
for s in uidb.self_signatures() {
acc.push(s.clone().into())
}
for s in uidb.self_revocations() {
acc.push(s.clone().into())
}
for s in uidb.other_revocations() {
acc.push(s.clone().into())
}
// Reasoning about the currently attested certifications
// requires a policy.
@@ -70,7 +84,8 @@ pub fn tpk_clean(tpk: &Cert) -> Result<Cert> {
/// Filters the Cert, keeping only UserIDs that aren't revoked, and whose emails match the given list
pub fn tpk_filter_alive_emails(tpk: &Cert, emails: &[Email]) -> Cert {
tpk.clone().retain_userids(|uid| {
if is_status_revoked(uid.revocation_status(&POLICY, None)) {
let is_exportable = uid.self_signatures().any(|s| s.exportable().is_ok());
if !is_exportable || is_status_revoked(uid.revocation_status(&POLICY, None)) {
false
} else if let Ok(email) = Email::try_from(uid.userid()) {
emails.contains(&email)

782
database/src/sqlite.rs Normal file
View File

@@ -0,0 +1,782 @@
use self_cell::self_cell;
use std::convert::TryFrom;
use std::fs::create_dir_all;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use crate::types::{Email, Fingerprint, KeyID};
use crate::{Database, Query};
use anyhow::{anyhow, format_err};
use sequoia_openpgp::{Cert, policy::StandardPolicy};
use std::time::{SystemTime, UNIX_EPOCH};
use r2d2_sqlite::SqliteConnectionManager;
use r2d2_sqlite::rusqlite::OptionalExtension;
use r2d2_sqlite::rusqlite::ToSql;
use r2d2_sqlite::rusqlite::Transaction;
use r2d2_sqlite::rusqlite::params;
use crate::{DatabaseTransaction, wkd};
pub const POLICY: StandardPolicy = StandardPolicy::new();
const DEFAULT_DB_FILE_NAME: &str = "keys.sqlite";
const DEFAULT_LOG_DIR_NAME: &str = "log";
pub struct Sqlite {
pool: r2d2::Pool<SqliteConnectionManager>,
}
impl Sqlite {
pub fn new_file(db_file: impl AsRef<Path>, log_dir: impl AsRef<Path>) -> anyhow::Result<Self> {
create_dir_all(log_dir)?;
Self::new_internal(SqliteConnectionManager::file(db_file))
}
pub fn log_dir_path(base_dir: impl AsRef<Path>) -> PathBuf {
base_dir.as_ref().join(DEFAULT_LOG_DIR_NAME)
}
pub fn log_dir_path_from_db_file_path(
db_file_path: impl AsRef<Path>,
) -> anyhow::Result<PathBuf> {
db_file_path
.as_ref()
.parent()
.ok_or_else(|| {
anyhow!(
"Can't get log dir path from invalid db file path: {:?}",
db_file_path.as_ref()
)
})
.map(|parent_dir_path| parent_dir_path.join(DEFAULT_LOG_DIR_NAME))
}
pub fn db_file_path(base_dir: impl AsRef<Path>) -> PathBuf {
base_dir.as_ref().join(DEFAULT_DB_FILE_NAME)
}
#[cfg(test)]
fn build_pool(
manager: SqliteConnectionManager,
) -> anyhow::Result<r2d2::Pool<SqliteConnectionManager>> {
#[derive(Copy, Clone, Debug)]
pub struct LogConnectionCustomizer;
impl<E> r2d2::CustomizeConnection<rusqlite::Connection, E> for LogConnectionCustomizer {
fn on_acquire(&self, conn: &mut rusqlite::Connection) -> Result<(), E> {
println!("Acquiring sqlite pool connection: {:?}", conn);
conn.trace(Some(|query| {
println!("{}", query);
}));
Ok(())
}
fn on_release(&self, conn: rusqlite::Connection) {
println!("Releasing pool connection: {:?}", conn);
}
}
Ok(r2d2::Pool::builder()
.connection_customizer(Box::new(LogConnectionCustomizer {}))
.build(manager)?)
}
#[cfg(not(test))]
fn build_pool(
manager: SqliteConnectionManager,
) -> anyhow::Result<r2d2::Pool<SqliteConnectionManager>> {
Ok(r2d2::Pool::builder().build(manager)?)
}
fn new_internal(manager: SqliteConnectionManager) -> anyhow::Result<Self> {
let pool = Self::build_pool(manager)?;
let conn = pool.get()?;
conn.pragma_update(None, "journal_mode", "wal")?;
conn.pragma_update(None, "synchronous", "normal")?;
conn.pragma_update(None, "user_version", "1")?;
conn.execute_batch(
"
CREATE TABLE IF NOT EXISTS certs (
primary_fingerprint TEXT NOT NULL PRIMARY KEY,
full TEXT NOT NULL,
published TEXT,
published_not_armored BLOB,
updated_at TIMESTAMP NOT NULL,
created_at TIMESTAMP NOT NULL
);
CREATE TABLE IF NOT EXISTS cert_identifiers (
fingerprint TEXT NOT NULL PRIMARY KEY,
keyid TEXT NOT NULL,
primary_fingerprint TEXT NOT NULL,
created_at TIMESTAMP NOT NULL
);
CREATE TABLE IF NOT EXISTS emails (
email TEXT NOT NULL PRIMARY KEY,
domain TEXT NOT NULL,
wkd_hash TEXT NOT NULL,
primary_fingerprint TEXT NOT NULL,
created_at TIMESTAMP NOT NULL
);
",
)?;
Ok(Self { pool })
}
}
self_cell! {
pub struct SqliteTransaction {
owner: r2d2::PooledConnection<SqliteConnectionManager>,
#[covariant]
dependent: Transaction,
}
}
impl SqliteTransaction {
fn start(pool: &r2d2::Pool<SqliteConnectionManager>) -> anyhow::Result<Self> {
let conn = pool.get()?;
Ok(Self::new(conn, |c| {
Transaction::new_unchecked(c, rusqlite::TransactionBehavior::Deferred).unwrap()
}))
}
fn tx(&self) -> &Transaction<'_> {
self.borrow_dependent()
}
}
fn query_simple<T: rusqlite::types::FromSql>(
conn: &r2d2::PooledConnection<SqliteConnectionManager>,
query: &str,
params: &[&dyn ToSql],
) -> Option<T> {
conn.prepare_cached(query)
.expect("query must be valid")
.query_row(params, |row| row.get(0))
.optional()
.expect("query exection must not fail")
}
impl DatabaseTransaction<'_> for SqliteTransaction {
type TempCert = Vec<u8>;
fn commit(self) -> anyhow::Result<()> {
// we can't use tx().commit(), but we can cheat :)
self.tx().execute_batch("COMMIT")?;
Ok(())
}
fn write_to_temp(&self, content: &[u8]) -> anyhow::Result<Self::TempCert> {
Ok(content.to_vec())
}
fn move_tmp_to_full(&self, file: Self::TempCert, fpr: &Fingerprint) -> anyhow::Result<()> {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_millis() as u64;
let file = String::from_utf8(file)?;
self.tx().execute(
"
INSERT INTO certs (primary_fingerprint, full, created_at, updated_at)
VALUES (?1, ?2, ?3, ?3)
ON CONFLICT(primary_fingerprint) DO UPDATE SET full=excluded.full, updated_at = excluded.updated_at
",
params![fpr, file, now],
)?;
Ok(())
}
fn move_tmp_to_published(&self, file: Self::TempCert, fpr: &Fingerprint) -> anyhow::Result<()> {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_millis() as u64;
let file = String::from_utf8(file)?;
self.tx().execute(
"UPDATE certs SET published = ?2, updated_at = ?3 WHERE primary_fingerprint = ?1",
params![fpr, file, now],
)?;
Ok(())
}
fn move_tmp_to_published_wkd(
&self,
file: Option<Self::TempCert>,
fpr: &Fingerprint,
) -> anyhow::Result<()> {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_millis() as u64;
self.tx().execute(
"UPDATE certs SET published_not_armored = ?2, updated_at = ?3 WHERE primary_fingerprint = ?1",
params![fpr, file, now],
)?;
Ok(())
}
fn write_to_quarantine(&self, _fpr: &Fingerprint, _content: &[u8]) -> anyhow::Result<()> {
Ok(())
}
fn link_email(&self, email: &Email, fpr: &Fingerprint) -> anyhow::Result<()> {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_millis() as u64;
let (domain, wkd_hash) = wkd::encode_wkd(email.as_str()).expect("email must be vaild");
self.tx().execute(
"
INSERT INTO emails (email, wkd_hash, domain, primary_fingerprint, created_at)
VALUES (?1, ?2, ?3, ?4, ?5)
ON CONFLICT(email) DO UPDATE SET primary_fingerprint = excluded.primary_fingerprint
",
params![email, domain, wkd_hash, fpr, now],
)?;
Ok(())
}
fn unlink_email(&self, email: &Email, fpr: &Fingerprint) -> anyhow::Result<()> {
self.tx()
.execute(
"DELETE FROM emails WHERE email = ?1 AND primary_fingerprint = ?2",
params![email, fpr],
)
.unwrap();
Ok(())
}
fn link_fpr(&self, from_fpr: &Fingerprint, primary_fpr: &Fingerprint) -> anyhow::Result<()> {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_millis() as u64;
self.tx().execute(
"
INSERT INTO cert_identifiers (fingerprint, keyid, primary_fingerprint, created_at)
VALUES (?1, ?2, ?3, ?4)
ON CONFLICT(fingerprint) DO UPDATE SET primary_fingerprint = excluded.primary_fingerprint;
",
params![
from_fpr,
KeyID::from(from_fpr),
primary_fpr,
now,
],
)?;
Ok(())
}
fn unlink_fpr(&self, from_fpr: &Fingerprint, primary_fpr: &Fingerprint) -> anyhow::Result<()> {
self.tx().execute(
"DELETE FROM cert_identifiers WHERE primary_fingerprint = ?1 AND fingerprint = ?2 AND keyid = ?3",
params![primary_fpr, from_fpr, KeyID::from(from_fpr)],
)?;
Ok(())
}
}
impl<'a> Database<'a> for Sqlite {
type Transaction = SqliteTransaction;
fn transaction(&'a self) -> anyhow::Result<Self::Transaction> {
SqliteTransaction::start(&self.pool)
}
fn write_log_append(&self, _filename: &str, _fpr_primary: &Fingerprint) -> anyhow::Result<()> {
// this is done implicitly via created_at in sqlite, no need to do anything here
Ok(())
}
fn lookup_primary_fingerprint(&self, term: &Query) -> Option<Fingerprint> {
use super::Query::*;
let conn = self.pool.get().unwrap();
match term {
ByFingerprint(fp) => query_simple(
&conn,
"SELECT primary_fingerprint FROM cert_identifiers WHERE fingerprint = ?1",
params![fp],
),
ByKeyID(keyid) => query_simple(
&conn,
"SELECT primary_fingerprint FROM cert_identifiers WHERE keyid = ?1",
params![keyid],
),
ByEmail(email) => query_simple(
&conn,
"SELECT primary_fingerprint FROM emails WHERE email = ?1",
params![email],
),
_ => None,
}
}
// Lookup straight from certs table, no link resolution
fn by_fpr_full(&self, primary_fpr: &Fingerprint) -> Option<String> {
let conn = self.pool.get().unwrap();
query_simple(
&conn,
"SELECT full FROM certs WHERE primary_fingerprint = ?1",
params![primary_fpr],
)
}
// XXX: rename! to by_primary_fpr_published
// Lookup the published cert straight from certs table, no link resolution
fn by_primary_fpr(&self, primary_fpr: &Fingerprint) -> Option<String> {
let conn = self.pool.get().unwrap();
query_simple(
&conn,
"SELECT published FROM certs WHERE primary_fingerprint = ?1",
params![primary_fpr],
)
}
fn by_fpr(&self, fpr: &Fingerprint) -> Option<String> {
let conn = self.pool.get().unwrap();
query_simple::<Fingerprint>(
&conn,
"SELECT primary_fingerprint FROM cert_identifiers WHERE fingerprint = ?1",
params![fpr],
)
.and_then(|primary_fpr| {
query_simple(
&conn,
"SELECT published FROM certs WHERE primary_fingerprint = ?1",
params![&primary_fpr],
)
})
}
fn by_email(&self, email: &Email) -> Option<String> {
let conn = self.pool.get().unwrap();
query_simple::<Fingerprint>(
&conn,
"SELECT primary_fingerprint FROM emails WHERE email = ?1",
params![email],
)
.and_then(|primary_fpr| {
query_simple(
&conn,
"SELECT published FROM certs WHERE primary_fingerprint = ?1",
params![&primary_fpr],
)
})
}
fn by_email_wkd(&self, email: &Email) -> Option<Vec<u8>> {
let conn = self.pool.get().unwrap();
query_simple::<Fingerprint>(
&conn,
"SELECT primary_fingerprint FROM emails WHERE email = ?1",
params![email],
)
.and_then(|primary_fpr| {
query_simple(
&conn,
"SELECT published_not_armored FROM certs WHERE primary_fingerprint = ?1",
params![&primary_fpr],
)
})
}
fn by_kid(&self, kid: &KeyID) -> Option<String> {
let conn = self.pool.get().unwrap();
query_simple::<Fingerprint>(
&conn,
"SELECT primary_fingerprint FROM cert_identifiers WHERE keyid = ?1",
params![kid],
)
.and_then(|primary_fpr| {
query_simple(
&conn,
"SELECT published FROM certs WHERE primary_fingerprint = ?1",
params![primary_fpr],
)
})
}
fn by_domain_and_hash_wkd(&self, domain: &str, wkd_hash: &str) -> Option<Vec<u8>> {
let conn = self.pool.get().unwrap();
query_simple::<Fingerprint>(
&conn,
"SELECT primary_fingerprint FROM emails WHERE domain = ?1 AND wkd_hash = ?2",
params![domain, wkd_hash],
)
.and_then(|primary_fpr| {
query_simple(
&conn,
"SELECT published_not_armored FROM certs WHERE primary_fingerprint = ?1",
params![primary_fpr],
)
})
}
fn check_link_fpr(
&self,
fpr: &Fingerprint,
_fpr_target: &Fingerprint,
) -> anyhow::Result<Option<Fingerprint>> {
// a desync here cannot happen structurally, so always return true here
Ok(Some(fpr.clone()))
}
/// Checks the database for consistency.
///
/// Note that this operation may take a long time, and is
/// generally only useful for testing.
fn check_consistency(&self) -> anyhow::Result<()> {
let conn = self.pool.get().unwrap();
let mut stmt = conn.prepare("SELECT primary_fingerprint, published FROM certs")?;
let mut rows = stmt.query([])?;
while let Some(row) = rows.next()? {
let primary_fpr: Fingerprint = row.get(0)?;
let published: String = row.get(1)?;
let cert = Cert::from_str(&published).unwrap();
let mut cert_emails: Vec<Email> = cert
.userids()
.filter_map(|uid| uid.userid().email2().unwrap())
.flat_map(Email::from_str)
.collect();
let mut db_emails: Vec<Email> = conn
.prepare("SELECT email FROM emails WHERE primary_fingerprint = ?1")?
.query_map([&primary_fpr], |row| row.get::<_, String>(0))
.unwrap()
.flat_map(|email| Email::from_str(&email.unwrap()))
.collect();
cert_emails.sort();
cert_emails.dedup();
db_emails.sort();
if cert_emails != db_emails {
return Err(format_err!(
"{:?} does not have correct emails indexed, cert ${:?} db {:?}",
&primary_fpr,
cert_emails,
db_emails,
));
}
let policy = &POLICY;
let mut cert_fprs: Vec<Fingerprint> = cert
.keys()
.with_policy(policy, None)
.for_certification()
.for_signing()
.map(|amalgamation| amalgamation.key().fingerprint())
.flat_map(Fingerprint::try_from)
.collect();
let mut db_fprs: Vec<Fingerprint> = conn
.prepare("SELECT fingerprint FROM cert_identifiers WHERE primary_fingerprint = ?1")?
.query_map([&primary_fpr], |row| row.get::<_, Fingerprint>(0))
.unwrap()
.flatten()
.collect();
cert_fprs.sort();
db_fprs.sort();
if cert_fprs != db_fprs {
return Err(format_err!(
"{:?} does not have correct fingerprints indexed, cert ${:?} db {:?}",
&primary_fpr,
cert_fprs,
db_fprs,
));
}
}
Ok(())
}
fn get_last_log_entry(&self) -> anyhow::Result<Fingerprint> {
let conn = self.pool.get().unwrap();
Ok(conn.query_row(
"SELECT primary_fingerprint FROM certs ORDER BY updated_at DESC LIMIT 1",
[],
|row| row.get::<_, Fingerprint>(0),
)?)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test;
use sequoia_openpgp::cert::CertBuilder;
use tempfile::TempDir;
const DATA_1: &str = "data, content doesn't matter";
const DATA_2: &str = "other data, content doesn't matter";
const FINGERPRINT_1: &str = "D4AB192964F76A7F8F8A9B357BD18320DEADFA11";
fn open_db() -> (TempDir, Sqlite) {
let tmpdir = TempDir::new().unwrap();
let tempdir_path = tmpdir.path();
let db = Sqlite::new_file(
Sqlite::db_file_path(tempdir_path),
Sqlite::log_dir_path(tempdir_path),
)
.unwrap();
(tmpdir, db)
}
#[test]
fn new() {
let (_tmp_dir, db) = open_db();
let k1 = CertBuilder::new()
.add_userid("a@invalid.example.org")
.generate()
.unwrap()
.0;
let k2 = CertBuilder::new()
.add_userid("b@invalid.example.org")
.generate()
.unwrap()
.0;
let k3 = CertBuilder::new()
.add_userid("c@invalid.example.org")
.generate()
.unwrap()
.0;
assert!(
!db.merge(k1)
.unwrap()
.into_tpk_status()
.email_status
.is_empty()
);
assert!(
!db.merge(k2.clone())
.unwrap()
.into_tpk_status()
.email_status
.is_empty()
);
assert!(!db.merge(k2).unwrap().into_tpk_status().email_status.len() > 0);
assert!(
!db.merge(k3.clone())
.unwrap()
.into_tpk_status()
.email_status
.is_empty()
);
assert!(
!db.merge(k3.clone())
.unwrap()
.into_tpk_status()
.email_status
.len()
> 0
);
assert!(!db.merge(k3).unwrap().into_tpk_status().email_status.len() > 0);
}
#[test]
fn xx_by_fpr_full() -> anyhow::Result<()> {
let (_tmp_dir, db) = open_db();
let fpr1 = Fingerprint::from_str(FINGERPRINT_1)?;
let lock = db.transaction().unwrap();
lock.move_tmp_to_full(lock.write_to_temp(DATA_1.as_bytes())?, &fpr1)?;
lock.link_fpr(&fpr1, &fpr1)?;
lock.commit().unwrap();
assert_eq!(db.by_fpr_full(&fpr1).expect("must find key"), DATA_1);
Ok(())
}
#[test]
fn xx_by_kid() -> anyhow::Result<()> {
let (_tmp_dir, db) = open_db();
let fpr1 = Fingerprint::from_str(FINGERPRINT_1)?;
let lock = db.transaction().unwrap();
lock.move_tmp_to_full(lock.write_to_temp(DATA_1.as_bytes())?, &fpr1)?;
lock.move_tmp_to_published(lock.write_to_temp(DATA_2.as_bytes())?, &fpr1)?;
lock.link_fpr(&fpr1, &fpr1)?;
lock.commit().unwrap();
assert_eq!(db.by_kid(&fpr1.into()).expect("must find key"), DATA_2);
Ok(())
}
#[test]
fn xx_by_primary_fpr() -> anyhow::Result<()> {
let (_tmp_dir, db) = open_db();
let fpr1 = Fingerprint::from_str(FINGERPRINT_1)?;
let lock = db.transaction().unwrap();
lock.move_tmp_to_full(lock.write_to_temp(DATA_1.as_bytes())?, &fpr1)?;
lock.move_tmp_to_published(lock.write_to_temp(DATA_2.as_bytes())?, &fpr1)?;
lock.commit().unwrap();
assert_eq!(db.by_primary_fpr(&fpr1).expect("must find key"), DATA_2);
Ok(())
}
#[test]
fn uid_verification() {
let (_tmp_dir, mut db) = open_db();
test::test_uid_verification(&mut db);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn uid_deletion() {
let (_tmp_dir, mut db) = open_db();
test::test_uid_deletion(&mut db);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn subkey_lookup() {
let (_tmp_dir, mut db) = open_db();
test::test_subkey_lookup(&mut db);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn kid_lookup() {
let (_tmp_dir, mut db) = open_db();
test::test_kid_lookup(&mut db);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn upload_revoked_tpk() {
let (_tmp_dir, mut db) = open_db();
test::test_upload_revoked_tpk(&mut db);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn uid_revocation() {
let (_tmp_dir, mut db) = open_db();
test::test_uid_revocation(&mut db);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn regenerate() {
let (_tmp_dir, mut db) = open_db();
test::test_regenerate(&mut db);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn key_reupload() {
let (_tmp_dir, mut db) = open_db();
test::test_reupload(&mut db);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn uid_replacement() {
let (_tmp_dir, mut db) = open_db();
test::test_uid_replacement(&mut db);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn uid_unlinking() {
let (_tmp_dir, mut db) = open_db();
test::test_unlink_uid(&mut db);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn same_email_1() {
let (_tmp_dir, mut db) = open_db();
test::test_same_email_1(&mut db);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn same_email_2() {
let (_tmp_dir, mut db) = open_db();
test::test_same_email_2(&mut db);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn same_email_3() {
let (_tmp_dir, mut db) = open_db();
test::test_same_email_3(&mut db);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn same_email_4() {
let (_tmp_dir, mut db) = open_db();
test::test_same_email_4(&mut db);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn no_selfsig() {
let (_tmp_dir, mut db) = open_db();
test::test_no_selfsig(&mut db);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn allow_dks() {
let (_tmp_dir, mut db) = open_db();
test::test_allow_dks(&mut db);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn allow_revoked() {
let (_tmp_dir, mut db) = open_db();
test::test_allow_revoked(&mut db);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn bad_uids() {
let (_tmp_dir, mut db) = open_db();
test::test_bad_uids(&mut db);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn reverse_fingerprint_to_path() {
let tmpdir = TempDir::new().unwrap();
let tmpdir_path = tmpdir.path();
let db = Sqlite::new_file(
Sqlite::db_file_path(tmpdir_path),
Sqlite::log_dir_path(tmpdir_path),
)
.unwrap();
let _fp: Fingerprint = "CBCD8F030588653EEDD7E2659B7DD433F254904A".parse().unwrap();
// XXX: fixme
//assert_eq!(Sqlite::path_to_fingerprint(&db.link_by_fingerprint(&fp)),
// Some(fp.clone()));
db.check_consistency().expect("inconsistent database");
}
#[test]
fn attested_key_signatures() -> anyhow::Result<()> {
let (_tmp_dir, mut db) = open_db();
test::attested_key_signatures(&mut db)?;
db.check_consistency()?;
Ok(())
}
#[test]
fn nonexportable_sigs() -> anyhow::Result<()> {
let (_tmp_dir, mut db) = open_db();
test::nonexportable_sigs(&mut db)?;
db.check_consistency()?;
Ok(())
}
}

View File

@@ -1,17 +1,16 @@
use std::io::{Read,Write};
use std::fs::{File, create_dir_all, remove_file};
use std::io::{Read, Write};
use std::path::PathBuf;
use std::fs::{create_dir_all, remove_file, File};
use log::info;
use std::str;
use Result;
pub struct StatefulTokens {
token_dir: PathBuf,
}
impl StatefulTokens {
pub fn new(token_dir: impl Into<PathBuf>) -> Result<Self> {
pub fn new(token_dir: impl Into<PathBuf>) -> anyhow::Result<Self> {
let token_dir = token_dir.into();
create_dir_all(&token_dir)?;
@@ -21,9 +20,9 @@ impl StatefulTokens {
Ok(StatefulTokens { token_dir })
}
pub fn new_token(&self, token_type: &str, payload: &[u8]) -> Result<String> {
pub fn new_token(&self, token_type: &str, payload: &[u8]) -> anyhow::Result<String> {
use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng};
use rand::{Rng, thread_rng};
let mut rng = thread_rng();
// samples from [a-zA-Z0-9]
@@ -38,7 +37,7 @@ impl StatefulTokens {
Ok(name)
}
pub fn pop_token(&self, token_type: &str, token: &str) -> Result<String> {
pub fn pop_token(&self, token_type: &str, token: &str) -> anyhow::Result<String> {
let path = self.token_dir.join(token_type).join(token);
let buf = {
let mut fd = File::open(&path)?;

View File

@@ -4,8 +4,6 @@ use std::path::Path;
use fs2::FileExt;
use Result;
/// A minimalistic flock-based mutex.
///
/// This just barely implements enough what we need from a mutex.
@@ -14,7 +12,7 @@ pub struct FlockMutexGuard {
}
impl FlockMutexGuard {
pub fn lock(path: impl AsRef<Path>) -> Result<Self> {
pub fn lock(path: impl AsRef<Path>) -> anyhow::Result<Self> {
let file = File::open(path)?;
while let Err(e) = file.lock_exclusive() {
// According to flock(2), possible errors returned are:

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,16 @@
use std::convert::TryFrom;
use std::fmt;
use std::result;
use std::str::FromStr;
use openpgp::packet::UserID;
use anyhow::anyhow;
use hex::ToHex;
use r2d2_sqlite::rusqlite::types::FromSql;
use r2d2_sqlite::rusqlite::types::FromSqlError;
use r2d2_sqlite::rusqlite::types::FromSqlResult;
use r2d2_sqlite::rusqlite::types::ToSql;
use r2d2_sqlite::rusqlite::types::ValueRef;
use sequoia_openpgp::packet::UserID;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use anyhow::Error;
use {Result};
/// Holds a normalized email address.
///
@@ -26,11 +30,27 @@ impl Email {
}
}
impl TryFrom<&UserID> for Email {
type Error = Error;
impl FromSql for Email {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
value
.as_str()
.and_then(|s| Self::from_str(s).map_err(|_| FromSqlError::InvalidType))
}
}
fn try_from(uid: &UserID) -> Result<Self> {
if let Some(address) = uid.email()? {
impl ToSql for Email {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
Ok(rusqlite::types::ToSqlOutput::Borrowed(
rusqlite::types::ValueRef::Text(self.0.as_bytes()),
))
}
}
impl TryFrom<&UserID> for Email {
type Error = anyhow::Error;
fn try_from(uid: &UserID) -> anyhow::Result<Self> {
if let Some(address) = uid.email2()? {
let mut iter = address.split('@');
let localpart = iter.next().expect("Invalid email address");
let domain = iter.next().expect("Invalid email address");
@@ -41,7 +61,7 @@ impl TryFrom<&UserID> for Email {
.map_err(|e| anyhow!("punycode conversion failed: {:?}", e))?;
// TODO this is a hotfix for a lettre vulnerability. remove once fixed upstream.
if localpart.starts_with("-") {
if localpart.starts_with('-') {
return Err(anyhow!("malformed email address: '{:?}'", uid.value()));
}
@@ -70,24 +90,39 @@ impl fmt::Display for Email {
}
impl FromStr for Email {
type Err = Error;
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Email> {
fn from_str(s: &str) -> anyhow::Result<Email> {
Email::try_from(&UserID::from(s))
}
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct Fingerprint([u8; 20]);
impl TryFrom<sequoia_openpgp::Fingerprint> for Fingerprint {
type Error = Error;
impl FromSql for Fingerprint {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
value
.as_str()
.and_then(|s| Self::from_str(s).map_err(|_| FromSqlError::InvalidType))
}
}
fn try_from(fpr: sequoia_openpgp::Fingerprint) -> Result<Self> {
impl ToSql for Fingerprint {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
Ok(rusqlite::types::ToSqlOutput::Owned(
rusqlite::types::Value::Text(self.to_string()),
))
}
}
impl TryFrom<sequoia_openpgp::Fingerprint> for Fingerprint {
type Error = anyhow::Error;
fn try_from(fpr: sequoia_openpgp::Fingerprint) -> anyhow::Result<Self> {
match fpr {
sequoia_openpgp::Fingerprint::V4(a) => Ok(Fingerprint(a)),
sequoia_openpgp::Fingerprint::Invalid(_) =>
Err(anyhow!("invalid fingerprint")),
sequoia_openpgp::Fingerprint::Invalid(_) => Err(anyhow!("invalid fingerprint")),
_ => Err(anyhow!("unknown fingerprint type")),
}
}
@@ -95,13 +130,12 @@ impl TryFrom<sequoia_openpgp::Fingerprint> for Fingerprint {
impl fmt::Display for Fingerprint {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use ::hex::ToHex;
self.0.write_hex_upper(f)
}
}
impl Serialize for Fingerprint {
fn serialize<S>(&self, serializer: S) -> result::Result<S::Ok, S::Error>
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
@@ -110,26 +144,26 @@ impl Serialize for Fingerprint {
}
impl<'de> Deserialize<'de> for Fingerprint {
fn deserialize<D>(deserializer: D) -> result::Result<Self, D::Error>
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::Error;
String::deserialize(deserializer).and_then(|string| {
Self::from_str(&string)
.map_err(|err| Error::custom(err.to_string()))
Self::from_str(&string).map_err(|err| Error::custom(err.to_string()))
})
}
}
impl FromStr for Fingerprint {
type Err = Error;
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Fingerprint> {
fn from_str(s: &str) -> anyhow::Result<Fingerprint> {
match sequoia_openpgp::Fingerprint::from_hex(s)? {
sequoia_openpgp::Fingerprint::V4(a) => Ok(Fingerprint(a)),
sequoia_openpgp::Fingerprint::Invalid(_) =>
Err(anyhow!("'{}' is not a valid fingerprint", s)),
sequoia_openpgp::Fingerprint::Invalid(_) => {
Err(anyhow!("'{}' is not a valid fingerprint", s))
}
_ => Err(anyhow!("unknown fingerprint type")),
}
}
@@ -138,15 +172,29 @@ impl FromStr for Fingerprint {
#[derive(Serialize, Deserialize, Clone, Debug, Hash, PartialEq, Eq)]
pub struct KeyID([u8; 8]);
impl TryFrom<sequoia_openpgp::Fingerprint> for KeyID {
type Error = Error;
impl FromSql for KeyID {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
value
.as_str()
.and_then(|s| Self::from_str(s).map_err(|_| FromSqlError::InvalidType))
}
}
fn try_from(fpr: sequoia_openpgp::Fingerprint) -> Result<Self> {
impl ToSql for KeyID {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
Ok(rusqlite::types::ToSqlOutput::Owned(
rusqlite::types::Value::Text(self.to_string()),
))
}
}
impl TryFrom<sequoia_openpgp::Fingerprint> for KeyID {
type Error = anyhow::Error;
fn try_from(fpr: sequoia_openpgp::Fingerprint) -> anyhow::Result<Self> {
match fpr {
sequoia_openpgp::Fingerprint::V4(a) => Ok(Fingerprint(a).into()),
sequoia_openpgp::Fingerprint::Invalid(_) => {
Err(anyhow!("invalid fingerprint"))
},
sequoia_openpgp::Fingerprint::Invalid(_) => Err(anyhow!("invalid fingerprint")),
_ => Err(anyhow!("unknown fingerprint type")),
}
}
@@ -172,19 +220,19 @@ impl From<Fingerprint> for KeyID {
impl fmt::Display for KeyID {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use ::hex::ToHex;
self.0.write_hex_upper(f)
}
}
impl FromStr for KeyID {
type Err = Error;
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<KeyID> {
fn from_str(s: &str) -> anyhow::Result<KeyID> {
match sequoia_openpgp::KeyID::from_hex(s)? {
sequoia_openpgp::KeyID::V4(a) => Ok(KeyID(a)),
sequoia_openpgp::KeyID::Invalid(_) =>
Err(anyhow!("'{}' is not a valid long key ID", s)),
sequoia_openpgp::KeyID::Invalid(_) => {
Err(anyhow!("'{}' is not a valid long key ID", s))
}
_ => Err(anyhow!("unknown keyid type")),
}
}
@@ -203,10 +251,11 @@ mod tests {
assert_eq!(c("Foo Bar <foo@example.org>").as_str(), "foo@example.org");
// FIXME gotta fix this
// assert_eq!(c("foo@example.org <foo@example.org>").as_str(), "foo@example.org");
assert_eq!(c("\"Foo Bar\" <foo@example.org>").as_str(),
"foo@example.org");
assert_eq!(c("foo@👍.example.org").as_str(),
"foo@xn--yp8h.example.org");
assert_eq!(
c("\"Foo Bar\" <foo@example.org>").as_str(),
"foo@example.org"
);
assert_eq!(c("foo@👍.example.org").as_str(), "foo@xn--yp8h.example.org");
assert_eq!(c("Foo@example.org").as_str(), "foo@example.org");
assert_eq!(c("foo@EXAMPLE.ORG").as_str(), "foo@example.org");
}

View File

@@ -1,11 +1,11 @@
use crate::openpgp::types::HashAlgorithm;
use anyhow::anyhow;
use sequoia_openpgp::types::HashAlgorithm;
use zbase32;
use super::Result;
// cannibalized from
// https://gitlab.com/sequoia-pgp/sequoia/blob/master/net/src/wkd.rs
pub fn encode_wkd(address: impl AsRef<str>) -> Result<(String,String)> {
pub fn encode_wkd(address: impl AsRef<str>) -> anyhow::Result<(String, String)> {
let (local_part, domain) = split_address(address)?;
let local_part_encoded = encode_local_part(local_part);
@@ -13,11 +13,11 @@ pub fn encode_wkd(address: impl AsRef<str>) -> Result<(String,String)> {
Ok((local_part_encoded, domain))
}
fn split_address(email_address: impl AsRef<str>) -> Result<(String,String)> {
fn split_address(email_address: impl AsRef<str>) -> anyhow::Result<(String, String)> {
let email_address = email_address.as_ref();
let v: Vec<&str> = email_address.split('@').collect();
if v.len() != 2 {
Err(anyhow!("Malformed email address".to_owned()))?;
return Err(anyhow!("Malformed email address".to_owned()));
};
// Convert to lowercase without tailoring, i.e. without taking any

38
default.nix Normal file
View File

@@ -0,0 +1,38 @@
{ lib, rustPlatform, sqlite, openssl, gettext, pkg-config, commitShaShort ? "" }:
rustPlatform.buildRustPackage rec {
pname = "hagrid";
version = "2.1.0";
src = ./.;
cargoLock = {
lockFile = ./Cargo.lock;
outputHashes = {
"rocket_i18n-0.5.0" = "sha256-EbUE8Z3TQBnDnptl9qWK6JvsACCgP7EXTxcA7pouYbc=";
};
};
postInstall = ''
cp -r dist $out
'';
nativeBuildInputs = [
pkg-config
gettext
];
buildInputs = [
sqlite
openssl
];
COMMIT_SHA_SHORT = commitShaShort;
meta = with lib; {
description = "A verifying keyserver";
homepage = "https://gitlab.com/keys.openpgp.org/hagrid";
license = with licenses; [ gpl3 ];
maintainers = with maintainers; [ valodim ];
platforms = platforms.all;
};
}

BIN
dist/assets/img/koo.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

23
dist/assets/img/koo.svg vendored Normal file
View File

@@ -0,0 +1,23 @@
<svg width="161" height="163" viewBox="0 0 161 163" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="79.6786" cy="81.6786" r="15.6786" fill="#4FA5FC" opacity="0.2"/>
<circle cx="16" cy="17" r="16" fill="#4FA5FC" opacity="0.2"/>
<circle cx="145" cy="146" r="16" fill="#4FA5FC" opacity="0.2"/>
<circle cx="144.5" cy="16.5" r="15.5" fill="#4FA5FC" opacity="0.2"/>
<circle cx="16" cy="81" r="16" fill="#4FA5FC" opacity="0.2"/>
<circle cx="16" cy="146" r="16" fill="#4FA5FC" opacity="0.2"/>
<rect width="32" height="162" rx="16" fill="url(#paint0_linear_2403_93)"/>
<circle cx="79.6786" cy="146.679" r="14.1108" stroke="#4FA5FC" stroke-width="3.13573" opacity="0.2"/>
<circle cx="79.6786" cy="17.6786" r="14.1108" stroke="#4FA5FC" stroke-width="3.13573" opacity="0.2"/>
<circle cx="144.679" cy="81.6786" r="14.1108" stroke="#4FA5FC" stroke-width="3.13573" opacity="0.2"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M64.0032 81.6796C63.9199 77.4791 65.481 73.2521 68.6863 70.0468L133.047 5.68629C139.295 -0.5621 149.426 -0.562094 155.674 5.68629C161.923 11.9347 161.923 22.0653 155.674 28.3137L102.308 81.6802L155.674 135.047C161.923 141.295 161.923 151.426 155.674 157.674C149.426 163.923 139.295 163.923 133.047 157.674L68.6863 93.3137C65.4806 90.108 63.9196 85.8805 64.0032 81.6796Z" fill="url(#paint1_linear_2403_93)"/>
<defs>
<linearGradient id="paint0_linear_2403_93" x1="16" y1="0" x2="16" y2="162" gradientUnits="userSpaceOnUse">
<stop stop-color="#4FA5FC" stop-opacity="0"/>
<stop offset="1" stop-color="#4FA5FC"/>
</linearGradient>
<linearGradient id="paint1_linear_2403_93" x1="116.873" y1="162.373" x2="116.873" y2="1.37259" gradientUnits="userSpaceOnUse">
<stop stop-color="#4FA5FC" stop-opacity="0"/>
<stop offset="1" stop-color="#4FA5FC"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

18
dist/assets/site.css vendored
View File

@@ -4,6 +4,7 @@ body {
font-family: 'Roboto', sans-serif;
font-weight: 300;
color: #050505;
word-wrap: break-word;
}
/* roboto-300 - latin-ext */
@@ -90,7 +91,6 @@ span.brand {
h1 {
padding-bottom: 0.75em;
padding-bottom: 0.75em;
}
.ui p {
@@ -110,6 +110,12 @@ a.brand {
color: #050505;
}
a.brand img {
height: 1.2em;
margin-right: 20px;
vertical-align: text-top;
}
.usage > h2 a, .usage > h2 a:visited {
color: #050505;
}
@@ -184,6 +190,13 @@ abbr {
padding: 0 15%;
}
@media screen and (min-width: 450px) {
.search, .upload, .manage {
display: flex;
padding: 0 15%;
}
}
.manageEmail, .searchTerm, .fileUpload {
flex-grow: 1;
border: 3px solid;
@@ -325,11 +338,10 @@ span.email {
bottom: 0;
right: 0;
left: 0;
width: 100%;
text-align: end;
font-size: 12px;
color: #bbb;
margin: 10px;
margin: 10px 20px;
}
.attribution a {

View File

@@ -75,7 +75,7 @@
<p>
Technically,
<span class="brand">keys.openpgp.org</span> runs on the <a href="https://gitlab.com/hagrid-keyserver/hagrid" target="_blank">Hagrid</a> keyserver software,
<span class="brand">keys.openpgp.org</span> runs on the <a href="https://gitlab.com/keys.openpgp.org/hagrid" target="_blank">Hagrid</a> keyserver software,
which is based on <a href="https://sequoia-pgp.org">Sequoia-PGP</a>.
We are running on <a href="https://eclips.is" target="_blank">eclips.is</a>,
a hosting platform focused on Internet Freedom projects,

View File

@@ -273,7 +273,7 @@
<li>No support for <code>op=vindex</code>.</li>
<li>Only exact matches by email address, fingerprint or long key id are returned.</li>
<li>All requests return either one or no keys.</li>
<li>The expiration date field in <code>op=index</code> is left blank (discussion <a target="_blank" href="https://gitlab.com/hagrid-keyserver/hagrid/issues/134">here</a>).</li>
<li>The expiration date field in <code>op=index</code> is left blank (discussion <a target="_blank" href="https://gitlab.com/keys.openpgp.org/hagrid/issues/134">here</a>).</li>
<li>All parameters and options other than <code>op</code> and <code>search</code> are ignored.</li>
<li>Output is always machine readable (i.e. <code>options=mr</code> is always assumed).</li>
<li>Uploads are restricted to 1 MiB.</li>

View File

@@ -2,6 +2,55 @@
<div class="about">
<center><h2><a href="/about">About</a> | News | <a href="/about/usage">Usage</a> | <a href="/about/faq">FAQ</a> | <a href="/about/stats">Stats</a> | <a href="/about/privacy">Privacy</a></h2></center>
<h2 id="2023-04-28-governance">
<div style="float: right; font-size: small; line-height: 2em;">2023-04-28 📅</div>
<a style="color: black;" href="/about/news#2023-04-28-governance">keys.openpgp.org governance 📜</a>
</h2>
<p>
It's been quite a while since the last update.
Not a lot happened around <span class="brand">keys.openpgp.org</span> during this time, operationally. 😴
<p>
But no news is good news in this case:
A few bugs were fixed, some software maintenance was perfomed to keep up with the ecosystem.
There were no significant outages, we've had some steady growth of users, things are generally working as expected.
Hurray!
<p>
There is, however, an important bit of news:
<span class="brand">keys.openpgp.org</span> has a governance process now.
In particular, there is now a written constitution for the service,
which you can find <a href="https://gitlab.com/keys.openpgp.org/governance/-/blob/main/constitution.md">here</a>.
<p>
Most importantly, there is now a board, who were elected by a community of contributors to the OpenPGP ecosystem.
This board currently consists of:
<ul>
<li>Daniel Huigens, from Proton</li>
<li>Lukas Pitschl, from GPGTools</li>
<li>Neal Walfield, from Sequoia-PGP</li>
<li>Ola Bini</li>
<li>Vincent Breitmoser</li>
</ul>
<p>
The primary responsibility of the board is to make decisions on the future of <span class="brand">keys.openpgp.org</span>.
Which features should go in, which not?
We are having regular meetings at the moment, and progress is slow but steady.
We'll be sure to let you know (via this news blog) when anything exciting happens!
<p>
You can find more info about governance in the <a href="https://gitlab.com/keys.openpgp.org/governance/">repository</a>.
You can also reach the board via email at <tt>board</tt> <tt>at</tt> <tt>keys.openpgp.org</tt>.
<p>
That's all for now!
<span style="font-size: x-large;">🙇</span>
<hr style="margin-top: 2em; margin-bottom: 2em;" />
<h2 id="2019-11-12-celebrating-100k">
<div style="float: right; font-size: small; line-height: 2em;">2019-11-12 📅</div>
<a style="color: black;" href="/about/news#2019-11-12-celebrating-100k">Celebrating 100.000 verified addresses! 📈</a>
@@ -30,7 +79,7 @@
</li>
<li>
We have been working on
a <strong><a target="_blank" href="https://gitlab.com/hagrid-keyserver/hagrid/issues/131">new mechanism to refresh keys</a></strong>
a <strong><a target="_blank" href="https://gitlab.com/keys.openpgp.org/hagrid/issues/131">new mechanism to refresh keys</a></strong>
that better protects the user's privacy.
</li>
<li>
@@ -133,7 +182,7 @@
<a href="https://blog.torproject.org/whats-new-tor-0298" target="_blank">single-hop</a>
mode on our Tor Onion Service.
You can find a more complete list
<a href="https://gitlab.com/hagrid-keyserver/hagrid/merge_requests?scope=all&utf8=%E2%9C%93&state=merged" target="_blank">here</a>.
<a href="https://gitlab.com/keys.openpgp.org/hagrid/merge_requests?scope=all&utf8=%E2%9C%93&state=merged" target="_blank">here</a>.
<h4>Secure email delivery with MTA-STS</h4>

View File

@@ -2,67 +2,103 @@
<div class="about">
<center><h2><a href="/about">About</a> | <a href="/about/news">News</a> | <a href="/about/usage">Usage</a> | <a href="/about/faq">FAQ</a> | <a href="/about/stats">Stats</a> | Privacy</h2></center>
<p style="text-align: left;">
The public keyserver running on keys.openpgp.org processes, stores and
distributes OpenPGP key data. The specific way in which data is processed
differs by type as follows:
<ul>
<li><b>Email Addresses</b>
<p>Email addresses contained in <abbr title="Packet Tag 13">User
IDs</abbr> are personally identifiable information (PII).
Special care is taken to make sure they are used only with
consent:
<ul>
<li>
Publishing requires <a target="_blank"
href="https://en.wikipedia.org/wiki/Opt-in_email#Confirmed_opt-in_(COI)_/_Double_opt-in_(DOI)">double
opt-in</a> validation, to prove ownership of the
email address in question.
</li>
<li>Addresses are searchable by exact email address,
but not by associated name.</li>
<li>Enumeration of addresses is not possible.</li>
<li>Deletion of addresses is possible via simple proof
of ownership in an automated fashion, similar to
publication. To unlist an address where this isn't
possible, write to support at keys dot openpgp dot
org.
</li>
</ul>
</p>
<p>This data is never handed collectively ("as a dump") to third
parties.
</p>
</li>
<li><b>Public Key Data</b>
<p>The cryptographic content of OpenPGP keys is not considered personally
identifiable information. This includes specifically
<abbr title="Packet Tags 6 and 14">public key material</abbr>,
<abbr title="Packet Tag 2, Signature types 0x10-0x13, 0x18, 0x19, 0x1F">self-signatures</abbr>, and
<abbr title="Packet Tag 2, Signature types 0x20, 0x28, 0x30">revocation signatures</abbr>.
</p>
<p>This data is not usually collectively available ("as
a dump"), but may be handed upon request to third
parties for purposes of development or research.
</p>
</li>
<li><b>Other User ID data</b>
<p>An OpenPGP key may contain personal data other than email
addresses, such as <abbr title="Packet Tag 13">User IDs</abbr>
that do not contain email addresses, or <abbr
title="Packet Tag 17">image attributes</abbr>. This data is stripped
during upload and never stored, processed, or distributed in
any way.
</p>
<p>OpenPGP packet types that were not specifically mentioned above are
stripped during upload and never stored, processed or
distributed in any way.
</p>
</li>
</ul>
<p style="text-align: left">Data is never relayed to third parties outside of
what is available from the <a href="/about/api">public API interfaces</a>,
and what is described in this policy.
</p>
<h3>Name and contact details</h3>
<p>
<span class="brand">keys.openpgp.org</span> is a community effort.
You can find more information about us, and our contact, details <a href="https://keys.openpgp.org/about">here</a>.
</p>
<h3>How we process data</h3>
<p>
The public keyserver running on <span class="brand">keys.openpgp.org</span> processes, stores, and distributes OpenPGP certificate data.
The specific way in which data is processed differs by type as follows:
</p>
<ul>
<li>
<h4>Email Addresses</h4>
<p>
Email addresses of individuals contained in <abbr title="Packet Tag 13">User IDs</abbr> are personal data.
Special care is taken to make sure they are used only with consent, which you can withdraw at any time:
</p>
<ul>
<li>Publishing requires double opt-in validation, to prove ownership of the email address in question.</li>
<li>Addresses are searchable by exact email address, but not by associated name.</li>
<li>Enumeration of addresses is not possible.</li>
<li>Deletion of addresses is possible via simple proof of ownership in an automated fashion, similar to publication, using the <a href="https://keys.openpgp.org/manage">“manage“ tool</a>. To unlist an address where this isn't possible, write to support at keys dot openpgp dot org.</li>
</ul>
<p>
This data is never handed collectively (“as a dump“) to third parties.
</p>
</li>
<li>
<h4>Public Key Data</h4>
<p>
We process the cryptographic content of OpenPGP certificates - such as public key material, self-signatures, and revocation signatures for the legitimate interest of providing the service.
</p>
<p>
This data is not usually collectively available (“as a dump), but may be handed upon request to third parties for purposes of development or research.
</p>
<p>
If you upload your OpenPGP certificates to the service, you are the source of this data.
It is also possible for anyone who has your public OpenPGP certificate to upload them to this service for example, if you have published them somewhere else, or sent them to someone. This does not include publication of Email Addresses, which are only used with explicit consent as described above.
</p>
</li>
<li>
<h4>Other User ID data</h4>
<p>
An OpenPGP certificate may contain personal data other than email addresses, such as User IDs that do not contain email addresses, or image attributes.
This data is stripped during upload and never stored, processed, or distributed in any way.
</p>
<p>
OpenPGP packet types that were not specifically mentioned above are stripped during upload and never stored, processed or distributed in any way.
</p>
</li>
</ul>
<p>
Data is never relayed to third parties outside of what is available from the public API interfaces, and what is described in this policy and on our <a href="https://keys.openpgp.org/about">about page</a>.
</p>
<p>
This service is available on the Internet, so anyone, anywhere in the world, can access it and retrieve data from it.
</p>
<h3>Retention periods</h3>
<p>
We will retain your email address linked with your OpenPGP certificates until you remove it.
We will remove your Public Key Data if you wish, but note that anyone can re-upload it to the service, in keeping with the “public” nature of this key material.
</p>
<p>
All incoming requests are logged for a period of 30 days, and only used as necessary for operation of the service.
IP addresses are anonymized for storage.
</p>
<h3>Your rights</h3>
<p>
You can withdraw consent to the processing of your email address at any time, or erase your email addresses, using the <a href="https://keys.openpgp.org/manage">“manage“ tool</a>.
</p>
<p>
You can obtain access to the personal data we process about you by viewing your OpenPGP certificates, or searching for your certificates using your email addresses, using this service.
</p>
<p>
You can delete your OpenPGP certificates by emailing support at keys dot openpgp dot org, but note that anyone can upload them again. If you object to having your certificate re-uploaded, email support at keys dot openpgp dot org and we will banlist your keys.
</p>
<p>
To exercise the right of portability, you can download your OpenPGP certificate using this service.
</p>
<p>
If you are in the EEA or UK, you also have the right to lodge a complaint with a supervisory authority, such as your local data protection authority.
</p>
</div>
{{/layout}}

View File

@@ -3,7 +3,13 @@
<title>keys.openpgp.org</title>
<link href="{{ base_uri }}/atom.xml" rel="self"/>
<id>urn:uuid:8e783366-73b1-460e-83d3-42f01046646d</id>
<updated>2019-11-12T12:00:00Z</updated>
<updated>2023-04-28T12:00:00Z</updated>
<entry>
<title>k.o.o governance 📜</title>
<link href="{{ base_uri }}/about/news#2023-04-28-governance" />
<updated>2023-04-28T12:00:00Z</updated>
<id>urn:uuid:75dfcd1e-ac6a-4d1b-9d0f-0e1821322f87</id>
</entry>
<entry>
<title>Celebrating 100.000 verified addresses! 📈</title>
<link href="{{ base_uri }}/about/news#2019-11-12-celebrating-100k" />

View File

@@ -25,7 +25,7 @@
<hr />
<p>
<strong>{{ text "News:" }}</strong> {{ text "<a href=\"/about/news#2019-11-12-celebrating-100k\">Celebrating 100.000 verified addresses! 📈</a> (2019-11-12)" }}
<strong>{{ text "News:" }}</strong> {{ text "<a href=\"/about/news#2023-04-28-governance\">keys.openpgp.org governance 📜</a> (2023-04-28)" }}
</p>
{{/with}}
{{/layout}}

View File

@@ -4,19 +4,20 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="/assets/site.css?v=19" type="text/css"/>
<link rel="alternate" href="/atom.xml" type="application/atom+xml" title="keys.openpgp.org newsfeed" />
<link rel="icon" href="/assets/img/koo.ico" />
<title>keys.openpgp.org</title>
</head>
<body lang="{{lang}}">
<div class="card">
<h1><a class="brand" href="/">keys.openpgp.org</a></h1>
<h1><a class="brand" href="/"><img src="/assets/img/koo.svg" alt="">keys.openpgp.org</a></h1>
{{> @partial-block }}
<div class="spacer"></div>
</div>
<div class="attribution">
<p>
<a href="https://gitlab.com/hagrid-keyserver/hagrid/">Hagrid</a>
<a href="https://gitlab.com/keys.openpgp.org/hagrid/">Hagrid</a>
{{ text "v{{ version }} built from" rerender }}
<a href="https://gitlab.com/hagrid-keyserver/hagrid/commit/{{ commit }}">{{ commit }}</a>
<a href="https://gitlab.com/keys.openpgp.org/hagrid/commit/{{ commit }}">{{ commit }}</a>
</p>
<p>{{ text "Powered by <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>" }}</p>
<p>{{ text "Background image retrieved from <a href=\"https://www.toptal.com/designers/subtlepatterns/subtle-grey/\">Subtle Patterns</a> under CC BY-SA 3.0" }}</p>

61
flake.lock generated Normal file
View File

@@ -0,0 +1,61 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1750005367,
"narHash": "sha256-h/aac1dGLhS3qpaD2aZt25NdKY7b+JT0ZIP2WuGsJMU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "6c64dabd3aa85e0c02ef1cdcb6e1213de64baee3",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-25.05",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs",
"utils": "utils"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

25
flake.nix Normal file
View File

@@ -0,0 +1,25 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, utils }:
utils.lib.eachDefaultSystem (system: let
pkgs = nixpkgs.legacyPackages."${system}";
commitShaShort = if self ? rev then (pkgs.lib.substring 0 10 self.rev) else self.dirtyShortRev;
in rec {
packages.hagrid = pkgs.callPackage ./. { inherit commitShaShort; };
packages.hagridctl = pkgs.callPackage ./hagridctl.nix { };
packages.wkdDomainChecker = pkgs.callPackage ./wkd-domain-checker/. { };
packages.default = packages.hagrid;
}) // {
overlays.hagrid = (final: prev: {
hagrid = self.packages."${final.system}".hagrid;
hagridctl = self.packages."${final.system}".hagridctl;
});
overlays.wkdDomainChecker = (final: prev: { wkdDomainChecker = self.packages."${final.system}".wkdDomainChecker; });
overlays.default = self.overlays.hagrid;
};
}

View File

@@ -12,23 +12,8 @@ limit_req_status 429;
# See https://gitlab.com/sequoia-pgp/hagrid/issues/94
error_page 502 =500 /502;
location /502 {
return 500;
}
# for x-accel-redirect forwards
location /keys {
internal;
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Cache-Control' 'no-cache' always;
etag off;
}
location /vks/v1/upload {
proxy_pass http://127.0.0.1:8080;
}
location /vks/v1/request-verify {
proxy_pass http://127.0.0.1:8080;
return 500;
}
location /vks {
@@ -36,12 +21,12 @@ location /vks {
limit_req zone=search_fpr_keyid burst=1000 nodelay;
error_page 404 /errors-static/404-by-fpr.htm;
default_type application/pgp-keys;
add_header Content-Disposition 'attachment; filename="$1$2$3.asc"';
# default_type application/pgp-keys;
# add_header Content-Disposition 'attachment; filename="$1$2$3.asc"';
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Cache-Control' 'no-cache' always;
etag off;
try_files /keys/links/by-fpr/$1/$2/$3 =404;
proxy_pass http://127.0.0.1:8080;
}
location ~ ^/vks/v1/by-keyid/(?:0x)?([^/][^/])([^/][^/])(.*)$ {
@@ -49,12 +34,12 @@ location /vks {
error_page 429 /errors-static/429-rate-limit-vks-fpr.htm;
error_page 404 /errors-static/404-by-keyid.htm;
default_type application/pgp-keys;
add_header Content-Disposition 'attachment; filename="$1$2$3.asc"';
# default_type application/pgp-keys;
# add_header Content-Disposition 'attachment; filename="$1$2$3.asc"';
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Cache-Control' 'no-cache' always;
etag off;
try_files /keys/links/by-keyid/$1/$2/$3 =404;
proxy_pass http://127.0.0.1:8080;
}
location /vks/v1/by-email/ {
@@ -110,12 +95,12 @@ location /.well-known/openpgpkey {
error_page 429 /errors-static/429-rate-limit-vks-email.htm;
error_page 404 /errors-static/404-wkd.htm;
default_type application/octet-stream;
add_header Content-Disposition 'attachment; filename="$2$3$4.asc"';
# default_type application/octet-stream;
# add_header Content-Disposition 'attachment; filename="$2$3$4.asc"';
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Cache-Control' 'no-cache' always;
etag off;
try_files /keys/links/wkd/$1/$2/$3/$4 =404;
proxy_pass http://127.0.0.1:8080;
}
location ~ "^/.well-known/openpgpkey/([^/]+)/policy$" {
@@ -227,26 +212,6 @@ location /search {
proxy_pass http://127.0.0.1:8080;
}
location /pks {
proxy_pass http://127.0.0.1:8080;
}
location /manage {
proxy_pass http://127.0.0.1:8080;
}
location /verify {
proxy_pass http://127.0.0.1:8080;
}
location /upload {
proxy_pass http://127.0.0.1:8080;
}
location /debug {
proxy_pass http://127.0.0.1:8080;
}
# explicitly cache the home directory
location = / {
proxy_cache static_cache;
@@ -264,3 +229,8 @@ location /about {
proxy_cache static_cache;
proxy_pass http://127.0.0.1:8080;
}
# all other locations are handled by hagrid
location / {
proxy_pass http://127.0.0.1:8080;
}

36
hagridctl.nix Normal file
View File

@@ -0,0 +1,36 @@
{ lib, rustPlatform, sqlite, openssl, gettext, pkg-config, commitShaShort ? "" }:
rustPlatform.buildRustPackage rec {
pname = "hagridctl";
version = "0.1.0";
src = ./.;
cargoLock = {
lockFile = ./Cargo.lock;
outputHashes = {
"rocket_i18n-0.5.0" = "sha256-EbUE8Z3TQBnDnptl9qWK6JvsACCgP7EXTxcA7pouYbc=";
};
};
nativeBuildInputs = [
pkg-config
gettext
];
buildInputs = [
sqlite
openssl
];
buildAndTestSubdir = "hagridctl";
COMMIT_SHA_SHORT = commitShaShort;
meta = with lib; {
description = "A verifying keyserver";
homepage = "https://gitlab.com/keys.openpgp.org/hagrid";
license = with licenses; [ gpl3 ];
maintainers = with maintainers; [ valodim ];
platforms = platforms.all;
};
}

View File

@@ -2,26 +2,28 @@
name = "hagridctl"
version = "0.1.0"
authors = ["Vincent Breitmoser <look@my.amazin.horse>"]
edition = "2024"
description = "Control hagrid database externally"
[dependencies]
hagrid-database = { path = "../database" }
anyhow = "1"
sequoia-openpgp = { version = "1.3", default-features = false, features = ["crypto-nettle"] }
multipart = "0"
log = "0"
rand = "0.6"
serde = { version = "1.0", features = ["derive"] }
serde_derive = "1.0"
serde_json = "1.0"
time = "0.1"
tempfile = "3.0"
url = "1.6"
hex = "0.3"
base64 = "0.10"
pathdiff = "0.1"
idna = "0.1"
fs2 = "0.4"
walkdir = "2.2"
clap = "2"
toml = "0.5.0"
indicatif = "0.11.0"
hagrid-database = { workspace = true }
anyhow = { workspace = true }
sequoia-openpgp = { workspace = true, features = ["crypto-openssl"] }
multipart = { workspace = true }
log = { workspace = true }
rand = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_derive = { workspace = true }
serde_json = { workspace = true }
time = { workspace = true }
tempfile = { workspace = true }
url = { workspace = true }
hex = { workspace = true }
base64 = { workspace = true }
pathdiff = { workspace = true }
idna = { workspace = true }
fs2 = { workspace = true }
walkdir = { workspace = true }
clap = { workspace = true, features = ["derive", "unicode", "env"] }
toml = { workspace = true }
indicatif = { workspace = true }

View File

@@ -1,41 +0,0 @@
[global]
address = "0.0.0.0"
port = 8080
[development]
base-URI = "http://localhost:8080"
from = "noreply@localhost"
x-accel-redirect = false
token_secret = "hagrid"
token_validity = 3600
template_dir = "dist/templates"
assets_dir = "dist/assets"
keys_internal_dir = "state/keys-internal"
keys_external_dir = "state/keys-external"
token_dir = "state/tokens"
tmp_dir = "state/tmp"
maintenance_file = "state/maintenance"
[staging]
base-URI = "https://keys.openpgp.org"
from = "noreply@keys.openpgp.org"
x-accel-redirect = true
template_dir = "templates"
keys_internal_dir = "keys"
keys_external_dir = "public/keys"
assets_dir = "public/assets"
token_dir = "tokens"
tmp_dir = "tmp"
maintenance_file = "maintenance"
[production]
base-URI = "https://keys.openpgp.org"
from = "noreply@keys.openpgp.org"
x-accel-redirect = true
template_dir = "templates"
keys_internal_dir = "keys"
keys_external_dir = "public/keys"
assets_dir = "public/assets"
token_dir = "tokens"
tmp_dir = "tmp"
maintenance_file = "maintenance"

73
hagridctl/src/cli.rs Normal file
View File

@@ -0,0 +1,73 @@
use crate::{delete, import};
use clap::{Parser, Subcommand};
use hagrid_database::Query;
use std::path::PathBuf;
#[derive(Parser)]
#[command(version, about, long_about = None, help_expected = true)]
pub(crate) struct Cli {
#[arg(long, required = false, env = "HAGRID_DB_FILE_PATH")]
/// Set a path to the Sqlite database file
db_file_path: PathBuf,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
/// Delete (address, key)-binding(s), and/or a key(s).
Delete {
#[arg(long)]
/// Also, delete all bindings
all_bindings: bool,
#[arg(long)]
/// Also, delete all bindings and the key
all: bool,
/// E-Mail address, Fingerprint, or KeyID of the TPK to delete.
/// If a Fingerprint or KeyID is given, --all is implied.
query: Query,
},
/// Import keys into Hagrid
Import {
#[arg(required = true)]
/// List of keyring files to import
keyring_files: Vec<PathBuf>,
},
}
pub(crate) fn dispatch_cmd(cli: &Cli) -> anyhow::Result<()> {
let db_file_path = cli.db_file_path.canonicalize()?;
match &cli.command {
Command::Delete {
query,
all_bindings,
all,
} => delete::run(db_file_path, query, *all_bindings, *all),
Command::Import { keyring_files } => import::run(db_file_path, keyring_files.to_owned()),
}
}
pub(crate) fn print_errors(e: anyhow::Error) {
eprint!("{}", e);
let mut cause = e.source();
while let Some(c) = cause {
eprint!(":\n {}", c);
cause = c.source();
}
eprintln!();
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory;
#[test]
fn test_cli() {
Cli::command().debug_assert()
}
}

87
hagridctl/src/delete.rs Normal file
View File

@@ -0,0 +1,87 @@
use hagrid_database::types::Fingerprint;
use hagrid_database::{Database, Query, Sqlite};
use std::convert::TryInto;
use std::path::Path;
pub(crate) fn run(
db_file_path: impl AsRef<Path>,
query: &Query,
all_bindings: bool,
mut all: bool,
) -> anyhow::Result<()> {
let db = &Sqlite::new_file(
&db_file_path,
Sqlite::log_dir_path_from_db_file_path(&db_file_path)?,
)?;
match query {
Query::ByFingerprint(_) | Query::ByKeyID(_) => {
eprintln!(
"Fingerprint or KeyID given, deleting key and all \
bindings."
);
all = true;
}
_ => (),
}
let tpk = db
.lookup(query)?
.ok_or_else(|| anyhow::format_err!("No TPK matching {:?}", query))?;
let fp: Fingerprint = tpk.fingerprint().try_into()?;
let mut results = Vec::new();
// First, delete the bindings.
if all_bindings || all {
results.push(("all bindings".into(), db.set_email_unpublished_all(&fp)));
} else if let Query::ByEmail(email) = query {
results.push((email.to_string(), db.set_email_unpublished(&fp, email)));
} else {
unreachable!()
}
// Now delete the key(s) itself.
if all {
// TODO
/*for skb in tpk.subkeys() {
results.push(
(skb.subkey().fingerprint().to_keyid().to_string(),
db.unlink_kid(&skb.subkey().fingerprint().try_into()?,
&fp)));
results.push(
(skb.subkey().fingerprint().to_string(),
db.unlink_fpr(&skb.subkey().fingerprint().try_into()?,
&fp)));
}
results.push(
(tpk.fingerprint().to_keyid().to_string(),
db.unlink_kid(&tpk.fingerprint().try_into()?,
&fp)));
results.push(
(tpk.fingerprint().to_string(),
db.update(&fp, None)));
*/
}
let mut err = Ok(());
for (slug, result) in results {
eprintln!(
"{}: {}",
slug,
if let Err(ref e) = result {
e.to_string()
} else {
"Deleted".into()
}
);
if err.is_ok() {
if let Err(e) = result {
err = Err(e);
}
}
}
err
}

View File

@@ -1,31 +1,25 @@
use std::path::{Path,PathBuf};
use std::cmp::min;
use std::convert::TryInto;
use std::fs::File;
use std::io::Read;
use std::thread;
use std::cmp::min;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::thread;
use anyhow::Result;
use sequoia_openpgp::Packet;
use sequoia_openpgp::parse::{PacketParser, PacketParserResult, Parse};
extern crate tempfile;
use hagrid_database::{Database, EmailAddressStatus, ImportResult, Sqlite};
extern crate sequoia_openpgp as openpgp;
use openpgp::Packet;
use openpgp::parse::{PacketParser, PacketParserResult, Parse};
extern crate hagrid_database as database;
use database::{Database, KeyDatabase, ImportResult};
use indicatif::{MultiProgress,ProgressBar,ProgressStyle};
use HagridConfig;
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
// parsing TPKs takes time, so we benefit from some parallelism. however, the
// database is locked during the entire merge operation, so we get diminishing
// returns after the first few threads.
const NUM_THREADS_MAX: usize = 3;
pub fn do_import(config: &HagridConfig, dry_run: bool, input_files: Vec<PathBuf>) -> Result<()> {
#[allow(clippy::needless_collect)]
pub fn run(db_file_path: impl AsRef<Path>, input_files: Vec<PathBuf>) -> anyhow::Result<()> {
let num_threads = min(NUM_THREADS_MAX, input_files.len());
let input_file_chunks = setup_chunks(input_files, num_threads);
@@ -35,11 +29,10 @@ pub fn do_import(config: &HagridConfig, dry_run: bool, input_files: Vec<PathBuf>
let threads: Vec<_> = input_file_chunks
.into_iter()
.map(|input_file_chunk| {
let config = config.clone();
let db_file_path = db_file_path.as_ref().to_owned();
let multi_progress = multi_progress.clone();
thread::spawn(move || {
import_from_files(
&config, dry_run, input_file_chunk, multi_progress).unwrap();
import_from_files(db_file_path, input_file_chunk, multi_progress).unwrap();
})
})
.collect();
@@ -53,15 +46,12 @@ pub fn do_import(config: &HagridConfig, dry_run: bool, input_files: Vec<PathBuf>
Ok(())
}
fn setup_chunks(
mut input_files: Vec<PathBuf>,
num_threads: usize,
) -> Vec<Vec<PathBuf>> {
let chunk_size = (input_files.len() + (num_threads - 1)) / num_threads;
fn setup_chunks(mut input_files: Vec<PathBuf>, num_threads: usize) -> Vec<Vec<PathBuf>> {
let chunk_size = input_files.len().div_ceil(num_threads);
(0..num_threads)
.map(|_| {
let len = input_files.len();
input_files.drain(0..min(chunk_size,len)).collect()
input_files.drain(0..min(chunk_size, len)).collect()
})
.collect()
}
@@ -76,7 +66,7 @@ struct ImportStats<'a> {
count_unchanged: u64,
}
impl <'a> ImportStats<'a> {
impl<'a> ImportStats<'a> {
fn new(progress: &'a ProgressBar, filename: String) -> Self {
ImportStats {
progress,
@@ -89,7 +79,7 @@ impl <'a> ImportStats<'a> {
}
}
fn update(&mut self, result: Result<ImportResult>) {
fn update(&mut self, result: anyhow::Result<ImportResult>) {
// If a new TPK starts, parse and import.
self.count_total += 1;
match result {
@@ -102,27 +92,29 @@ impl <'a> ImportStats<'a> {
}
fn progress_update(&self) {
if (self.count_total % 10) != 0 {
if !self.count_total.is_multiple_of(10) {
return;
}
self.progress.set_message(&format!(
"{}, imported {:5} keys, {:5} New {:5} Updated {:5} Unchanged {:5} Errors",
&self.filename, self.count_total, self.count_new, self.count_updated, self.count_unchanged, self.count_err));
"{}, imported {:5} keys, {:5} New {:5} Updated {:5} Unchanged {:5} Errors",
&self.filename,
self.count_total,
self.count_new,
self.count_updated,
self.count_unchanged,
self.count_err
));
}
}
fn import_from_files(
config: &HagridConfig,
dry_run: bool,
db_file_path: impl AsRef<Path>,
input_files: Vec<PathBuf>,
multi_progress: Arc<MultiProgress>,
) -> Result<()> {
let db = KeyDatabase::new_internal(
config.keys_internal_dir.as_ref().unwrap(),
config.keys_external_dir.as_ref().unwrap(),
config.tmp_dir.as_ref().unwrap(),
dry_run,
) -> anyhow::Result<()> {
let db = Sqlite::new_file(
&db_file_path,
Sqlite::log_dir_path_from_db_file_path(&db_file_path)?,
)?;
for input_file in input_files {
@@ -132,15 +124,20 @@ fn import_from_files(
Ok(())
}
fn import_from_file(db: &KeyDatabase, input: &Path, multi_progress: &MultiProgress) -> Result<()> {
fn import_from_file(
db: &Sqlite,
input: &Path,
multi_progress: &MultiProgress,
) -> anyhow::Result<()> {
let input_file = File::open(input)?;
let bytes_total = input_file.metadata()?.len();
let progress_bar = multi_progress.add(ProgressBar::new(bytes_total));
progress_bar
.set_style(ProgressStyle::default_bar()
progress_bar.set_style(
ProgressStyle::default_bar()
.template("[{elapsed_precise}] {bar:40.cyan/blue} {msg}")
.progress_chars("##-"));
.progress_chars("##-"),
);
progress_bar.set_message("Starting…");
let input_reader = &mut progress_bar.wrap_read(input_file);
@@ -149,16 +146,33 @@ fn import_from_file(db: &KeyDatabase, input: &Path, multi_progress: &MultiProgre
read_file_to_tpks(input_reader, &mut |acc| {
let primary_key = acc[0].clone();
let result = import_key(&db, acc);
let key_fpr = match primary_key {
Packet::PublicKey(key) => key.fingerprint(),
Packet::SecretKey(key) => key.fingerprint(),
_ => return,
};
let result = import_key(db, acc);
if let Ok(ref result) = result {
let tpk_status = result.as_tpk_status();
if !tpk_status.is_revoked {
for (email, status) in &tpk_status.email_status {
if status == &EmailAddressStatus::NotPublished {
db.set_email_published(&key_fpr.clone().try_into().unwrap(), email)
.unwrap();
}
}
}
}
if let Err(ref e) = result {
let key_fpr = match primary_key {
Packet::PublicKey(key) => key.fingerprint().to_hex(),
Packet::SecretKey(key) => key.fingerprint().to_hex(),
_ => "Unknown".to_owned(),
};
let error = format!("{}:{:05}:{}: {}\n{}", filename, stats.count_total,
key_fpr, e.to_string(), e.backtrace());
let error = format!(
"{}:{:05}:{}: {}",
filename,
stats.count_total,
key_fpr.to_hex(),
e
);
progress_bar.println(error);
return;
}
stats.update(result);
})?;
@@ -169,8 +183,8 @@ fn import_from_file(db: &KeyDatabase, input: &Path, multi_progress: &MultiProgre
fn read_file_to_tpks(
reader: impl Read + Send + Sync,
callback: &mut impl FnMut(Vec<Packet>) -> ()
) -> Result<()> {
callback: &mut impl FnMut(Vec<Packet>),
) -> anyhow::Result<()> {
let mut ppr = PacketParser::from_reader(reader)?;
let mut acc = Vec::new();
@@ -183,7 +197,7 @@ fn read_file_to_tpks(
if !acc.is_empty() {
if let Packet::PublicKey(_) | Packet::SecretKey(_) = packet {
callback(acc);
acc = vec!();
acc = vec![];
}
}
@@ -193,52 +207,6 @@ fn read_file_to_tpks(
Ok(())
}
fn import_key(db: &KeyDatabase, packets: Vec<Packet>) -> Result<ImportResult> {
openpgp::Cert::from_packets(packets.into_iter())
.and_then(|tpk| {
db.merge(tpk)
})
fn import_key(db: &Sqlite, packets: Vec<Packet>) -> anyhow::Result<ImportResult> {
sequoia_openpgp::Cert::from_packets(packets.into_iter()).and_then(|tpk| db.merge(tpk))
}
/*
#[cfg(test)]
mod import_tests {
use std::fs::File;
use tempfile::tempdir;
use openpgp::serialize::Serialize;
use super::*;
#[test]
fn import() {
let root = tempdir().unwrap();
let db = KeyDatabase::new_from_base(root.path().to_path_buf()).unwrap();
// Generate a key and import it.
let (tpk, _) = openpgp::tpk::TPKBuilder::autocrypt(
None, Some("foo@invalid.example.com".into()))
.generate().unwrap();
let import_me = root.path().join("import-me");
tpk.serialize(&mut File::create(&import_me).unwrap()).unwrap();
do_import(root.path().to_path_buf(), vec![import_me]).unwrap();
let check = |query: &str| {
let tpk_ = db.lookup(&query.parse().unwrap()).unwrap().unwrap();
assert_eq!(tpk.fingerprint(), tpk_.fingerprint());
assert_eq!(tpk.subkeys().map(|skb| skb.subkey().fingerprint())
.collect::<Vec<_>>(),
tpk_.subkeys().map(|skb| skb.subkey().fingerprint())
.collect::<Vec<_>>());
assert_eq!(tpk_.userids().count(), 0);
};
check(&format!("{}", tpk.primary().fingerprint()));
check(&format!("{}", tpk.primary().fingerprint().to_keyid()));
check(&format!("{}", tpk.subkeys().nth(0).unwrap().subkey()
.fingerprint()));
check(&format!("{}", tpk.subkeys().nth(0).unwrap().subkey()
.fingerprint().to_keyid()));
}
}
*/

View File

@@ -1,104 +1,13 @@
#![feature(proc_macro_hygiene, plugin, decl_macro)]
#![recursion_limit = "1024"]
extern crate anyhow;
extern crate clap;
extern crate tempfile;
extern crate sequoia_openpgp as openpgp;
extern crate hagrid_database as database;
#[macro_use]
extern crate serde_derive;
extern crate toml;
extern crate indicatif;
extern crate walkdir;
use std::fs;
use std::path::PathBuf;
use std::str::FromStr;
use anyhow::Result;
use clap::{Arg, App, SubCommand};
use clap::Parser;
mod cli;
mod delete;
mod import;
mod regenerate;
#[derive(Deserialize)]
pub struct HagridConfigs {
development: HagridConfig,
staging: HagridConfig,
production: HagridConfig,
}
const ERROR_EXIT_CODE: i32 = 2;
// this is not an exact match - Rocket config has more complicated semantics
// than a plain toml file.
// see also https://github.com/SergioBenitez/Rocket/issues/228
#[derive(Deserialize,Clone)]
pub struct HagridConfig {
_template_dir: Option<PathBuf>,
keys_internal_dir: Option<PathBuf>,
keys_external_dir: Option<PathBuf>,
_assets_dir: Option<PathBuf>,
_token_dir: Option<PathBuf>,
tmp_dir: Option<PathBuf>,
_maintenance_file: Option<PathBuf>,
}
fn main() -> Result<()> {
let matches = App::new("Hagrid Control")
.version("0.1")
.about("Control hagrid database externally")
.arg(Arg::with_name("config")
.short("c")
.long("config")
.value_name("FILE")
.help("Sets a custom config file")
.takes_value(true))
.arg(Arg::with_name("env")
.short("e")
.long("env")
.value_name("ENVIRONMENT")
.takes_value(true)
.default_value("prod")
.possible_values(&["dev","stage","prod"]))
.subcommand(SubCommand::with_name("regenerate")
.about("Regenerate symlink directory"))
.subcommand(SubCommand::with_name("import")
.about("Import keys into Hagrid")
.arg(Arg::with_name("dry run")
.short("n")
.long("dry-run")
.help("don't actually keep imported keys")
)
.arg(Arg::with_name("keyring files")
.required(true)
.multiple(true)))
.get_matches();
let config_file = matches.value_of("config").unwrap_or("Rocket.toml");
let config_data = fs::read_to_string(config_file).unwrap();
let configs: HagridConfigs = toml::from_str(&config_data).unwrap();
let config = match matches.value_of("env").unwrap() {
"dev" => configs.development,
"stage" => configs.staging,
"prod" => configs.production,
_ => configs.development,
fn main() {
let Ok(_) = cli::dispatch_cmd(&cli::Cli::parse()).map_err(cli::print_errors) else {
std::process::exit(ERROR_EXIT_CODE);
};
if let Some(matches) = matches.subcommand_matches("import") {
let dry_run = matches.occurrences_of("dry run") > 0;
let keyrings: Vec<PathBuf> = matches
.values_of_lossy("keyring files")
.unwrap()
.iter()
.map(|arg| PathBuf::from_str(arg).unwrap())
.collect();
import::do_import(&config, dry_run, keyrings)?;
} else if let Some(_matches) = matches.subcommand_matches("regenerate") {
regenerate::do_regenerate(&config)?;
} else {
println!("{}", matches.usage());
}
Ok(())
}

View File

@@ -1,123 +0,0 @@
use anyhow::Result;
use std::path::Path;
use std::time::Instant;
use walkdir::WalkDir;
use indicatif::{ProgressBar,ProgressStyle};
use HagridConfig;
use database::{Database,KeyDatabase,RegenerateResult};
use database::types::Fingerprint;
struct RegenerateStats<'a> {
progress: &'a ProgressBar,
prefix: String,
count_total: u64,
count_err: u64,
count_updated: u64,
count_unchanged: u64,
count_partial: u64,
start_time_partial: Instant,
kps_partial: u64,
}
impl <'a> RegenerateStats<'a> {
fn new(progress: &'a ProgressBar) -> Self {
Self {
progress,
prefix: "".to_owned(),
count_total: 0,
count_err: 0,
count_updated: 0,
count_unchanged: 0,
count_partial: 0,
start_time_partial: Instant::now(),
kps_partial: 0,
}
}
fn update(&mut self, result: Result<RegenerateResult>, fpr: Fingerprint) {
// If a new TPK starts, parse and import.
self.count_total += 1;
self.count_partial += 1;
if (self.count_total % 10) == 0 {
self.prefix = fpr.to_string()[0..4].to_owned();
}
match result {
Err(e) => {
self.progress.println(format!("{}: {}", fpr, e.to_string()));
self.count_err += 1;
},
Ok(RegenerateResult::Updated) => self.count_updated += 1,
Ok(RegenerateResult::Unchanged) => self.count_unchanged += 1,
}
self.progress_update();
}
fn progress_update(&mut self) {
if (self.count_total % 10) != 0 {
return;
}
if self.count_partial >= 1000 {
let runtime = (self.start_time_partial.elapsed().as_millis() + 1) as u64;
self.kps_partial = (self.count_partial * 1000) / runtime;
self.start_time_partial = Instant::now();
self.count_partial = 0;
}
self.progress.set_message(&format!(
"prefix {} regenerated {:5} keys, {:5} Updated {:5} Unchanged {:5} Errors ({:3} keys/s)",
self.prefix, self.count_total, self.count_updated, self.count_unchanged, self.count_err, self.kps_partial));
}
}
pub fn do_regenerate(config: &HagridConfig) -> Result<()> {
let db = KeyDatabase::new_internal(
config.keys_internal_dir.as_ref().unwrap(),
config.keys_external_dir.as_ref().unwrap(),
config.tmp_dir.as_ref().unwrap(),
false,
)?;
let published_dir = config.keys_external_dir.as_ref().unwrap().join("links").join("by-email");
let dirs: Vec<_> = WalkDir::new(published_dir)
.min_depth(1)
.max_depth(1)
.sort_by(|a,b| a.file_name().cmp(b.file_name()))
.into_iter()
.flatten()
.map(|entry| entry.into_path())
.collect();
let progress_bar = ProgressBar::new(dirs.len() as u64);
progress_bar
.set_style(ProgressStyle::default_bar()
.template("[{elapsed_precise}] {bar:40.cyan/blue} {msg}")
.progress_chars("##-"));
let mut stats = RegenerateStats::new(&progress_bar);
for dir in dirs {
progress_bar.inc(1);
regenerate_dir_recursively(&db, &mut stats, &dir)?;
}
progress_bar.finish();
Ok(())
}
fn regenerate_dir_recursively(db: &KeyDatabase, stats: &mut RegenerateStats, dir: &Path) -> Result<()> {
for path in WalkDir::new(dir)
.follow_links(true)
.into_iter()
.flatten()
.filter(|e| e.file_type().is_file())
.map(|entry| entry.into_path()) {
let fpr = KeyDatabase::path_to_primary(&path).unwrap();
let result = db.regenerate_links(&fpr);
stats.update(result, fpr);
}
Ok(())
}

383
justfile Normal file
View File

@@ -0,0 +1,383 @@
[private]
default:
@just --list
# ----------------Settings----------------
set fallback := true
# ----------------Variables----------------
SQLITE_DB_FILE_PATH := 'state/keys-internal/keys.sqlite'
# Regular expresion for sed cmd to match variants of Rust versions
# Matched Rust versions: 1, 1.90, 1.90.0
SED_RUST_VERSION_REGEX := '[0-9](\.[0-9]+){1,2}'
RUST_MANIFEST_STABLE_TOML_URL := 'https://static.rust-lang.org/dist/channel-rust-stable.toml'
# Hierarchycal path in the file to current stable version
RUST_MANIFEST_STABLE_VERSION_QUERY := 'pkg.rust.version'
# Regex in sed(-r) format
# Parsed string example: "1.90.0 (1159e78c4 2025-09-14)"
RUST_MANIFEST_STABLE_VERSION_PARSE_REGEX := '^"([0-9]+).([0-9]+).([0-9]+) \(([0-9A-Fa-f]+) (([0-9]+)-([0-9]+)-([0-9]+))\)"$'
# GOTCH: See documentation to `rust-stable-version` recipe for explanation on what \1, \2, etc. means.
DEFAULT_RUST_STABLE_VERSION_FORMAT := '\1.\2'
GITLAB_CI_FILE_NAME := '.gitlab-ci.yml'
GITLAB_CI_FILE_PATH := absolute_path(GITLAB_CI_FILE_NAME)
CARGO_FILE_NAME := 'Cargo.toml'
CARGO_FILE_PATH := absolute_path(CARGO_FILE_NAME)
CARGO_RUST_VERSION_QUERY := 'package.rust-version'
CLIPPY_FILE_NAME := 'clippy.toml'
CLIPPY_FILE_PATH := absolute_path(CLIPPY_FILE_NAME)
CLIPPY_RUST_VERSION_QUERY := 'msrv'
TOOLCHAIN_FILE_NAME := 'rust-toolchain.toml'
TOOLCHAIN_FILE_PATH := absolute_path(TOOLCHAIN_FILE_NAME)
TOOLCHAIN_RUST_VERSION_QUERY := 'toolchain.channel'
GIT_BRANCH_NAME_PREFIX := 'upgrade_rust_to_'
# ----------------Recipes----------------
# Perform initial setup of developer's system.
[group('setup')]
init: init-rocket-config
# Copy Rocket's template configuration from Rocket.toml.dist to Rocket.toml. Rocket is Rust web framework. See https://rocket.rs/guide/v0.5/configuration/#configuration
[group('setup')]
init-rocket-config:
#!/usr/bin/env -S bash -euo pipefail
[ ! -f Rocket.toml ] \
&& cp Rocket.toml.dist Rocket.toml \
&& echo "Rocket.toml.dist copied to Rocket.toml" \
|| echo "Rocket.toml exists already!"
# Format justfile
[group('fmt')]
[group('format')]
[group('just')]
just-fmt:
just --unstable --fmt
# Format Rust code in all packages (aka path based dependencies)
[group('fmt')]
[group('format')]
cargo-fmt:
cargo fmt --all
# Format all code
[group('fmt')]
[group('format')]
fmt: just-fmt cargo-fmt
alias f := fmt
# Check justfile formatting
[group('fmt')]
[group('just')]
[group('lint')]
just-lint-fmt:
just --unstable --fmt --check
# Check Rust code formatting in all packages (aka path based dependencies)
[group('fmt')]
[group('lint')]
cargo-lint-fmt:
cargo fmt --all -- --check
# Check formatting of all code
[group('fmt')]
[group('lint')]
lint-fmt: just-lint-fmt cargo-lint-fmt
alias lf := lint-fmt
# Lint Rust code with Clippy
[group('clippy')]
[group('lint')]
clippy-lint:
cargo clippy --tests --no-deps --workspace
alias cl := clippy-lint
# Lint all code
[group('lint')]
lint: lint-fmt clippy-lint
alias l := lint
# Fix compilation warnings by applying compiler suggestions
[group('fix')]
cargo-fix *args:
cargo fix --workspace {{ args }}
# Apply Clippy's lint suggestions, i.e. fix Clippy linting warnings or errors
[group('clippy')]
[group('fix')]
clippy-fix *args:
cargo clippy --fix --tests --no-deps --workspace {{ args }}
# Fix lint and compilation warnings and errors. Pass given arguments to all sub-recipes, i.e. `just fix --allow-dirty` calls `just cargo-fix --allow-dirty` and `just clippy-fix --allow-dirty`.
[group('fix')]
fix *args: (cargo-fix args) (clippy-fix args)
# Check Rust code errors
[group('compile')]
check:
cargo check
alias c := check
# Compile all Rust code
[group('compile')]
build *args='--workspace':
cargo build {{ args }}
alias b := build
# Run all tests (i.e. --workspace), but when args given pass them to `cargo test`, e.g. `just test fs::tests::init`
[group('test')]
test args='--workspace':
cargo test {{ args }}
alias t := test
# Run continuous check of Rust code errors. Detect file changes and repeat check automatically. Ctrl+c to exit. You can pass additional arguments, e.g. --notify (-N).
[group('compile')]
[group('watch')]
watch-check *args:
cargo watch --ignore *.pot {{ args }}
alias wc := watch-check
# Run web server and automatically restart on changes. Ctrl+c to exit. You can pass additional arguments, e.g. --notify (-N).
[group('compile')]
[group('run')]
[group('watch')]
watch-run *args:
cargo watch --exec 'run --bin hagrid' --ignore *.pot {{ args }}
alias wr := watch-run
# Run tests every time files changed. Ctrl+c to exit. You can pass additional arguments, e.g. --notify (-N).
[group('test')]
[group('watch')]
watch-test *args:
cargo watch --exec 'test --workspace' --ignore *.pot {{ args }}
alias wt := watch-test
# Run web server
[group('run')]
run:
cargo run
alias r := run
alias run-hagrid := run
alias hagrid := run
# Run "hagridctl" which automate some operations working with database externally, e.g. import keys
[group('run')]
run-hagridctl *args:
cargo run --package hagridctl -- {{ args }}
alias hagridctl := run-hagridctl
# Run "tester" which allows to seed database with sample data, e.g. for testing
[group('run')]
run-tester *args:
cargo run --package tester -- {{ args }}
alias tester := run-tester
# Clean compilation artifacts (i.e. "target" directory)
[group('clean')]
clean:
cargo clean
# Clean changes to translation files
[group('clean')]
[group('translation')]
clean-translations:
git restore po/
# Open database prompt
[group('database')]
@db:
command -v sqlite3 \
&& echo "See sqlite3 CLI Documentation: https://sqlite.org/cli.html\n" \
&& sqlite3 {{ SQLITE_DB_FILE_PATH }} \
|| echo "sqlite3 command has not been found. Please, install it using system's package manager or refer to documentation https://sqlite.org/cli.html for installation." >&2
# Translate *.hbs templates of web pages
[group('translation')]
translate-templates:
./make-translated-templates
# Upgrade Rust to a given version, by default current stable Rust version is used
[group('housekeeping')]
upgrade-rust version=`just _rust-stable-version`: _ensure-no-vcs-changes && _upgrade-rust-fixes-reminder
#!/usr/bin/env -S bash -euo pipefail
readonly OLD_VERSIONS=$( \
for recipe in _current-ci-rust-version _current-cargo-rust-version _current-clippy-rust-version _current-toolchain-rust-version; do \
just $recipe; \
done \
| sort -u \
); \
just _upgrade-rust-git-create-branch "{{ replace(version, '.', '_') }}" "{{ GIT_BRANCH_NAME_PREFIX + replace(version, '.', '_') }}"
for recipe in _upgrade-rust-ci _upgrade-rust-cargo _upgrade-rust-clippy _upgrade-rust-toolchain; do \
just $recipe '{{ version }}'; \
done \
just _upgrade-rust-git-commit \
"${OLD_VERSIONS//$'\n'/, }" \
"{{ version }}" \
"{{ GITLAB_CI_FILE_PATH }} {{ CARGO_FILE_PATH }} {{ CLIPPY_FILE_PATH }} {{ TOOLCHAIN_FILE_PATH }}" \
"{{ GITLAB_CI_FILE_NAME }} {{ CARGO_FILE_NAME }} {{ CLIPPY_FILE_NAME }} {{ TOOLCHAIN_FILE_NAME }}"
_ensure-no-vcs-changes:
#!/usr/bin/env -S bash -euo pipefail
readonly CHANGED_FILES=$(git ls-files --deleted --modified --others --exclude-standard -- :/); \
git diff-index --quiet HEAD -- >&2 && true
readonly HAS_STAGED_FILES=$?
if [ -n "$CHANGED_FILES" ] || [ $HAS_STAGED_FILES != 0 ]; \
then \
echo -e "{{ RED }}You have working directory changes! \nTo avoid loosing or corrupting your changes commit or stash (git stash -u) them before running commands which change code automatically!{{ NORMAL }}"; \
exit 1; \
fi
_upgrade-rust-git-create-branch branched_version branch_name:
#!/usr/bin/env -S bash -euo pipefail
readonly CURRENT_BRANCH=$(git branch --show-current)
if [[ "$CURRENT_BRANCH" == *{{ branched_version }}* ]]; then
echo "{{ GREEN }}It looks like you switched to new branch manually. Continue...{{ NORMAL }}"
exit 0
fi
while true; do
read -p "Would you like to create new branch ({{ branch_name }})? [y/n]: " input
case "$input" in
y)
break
;;
n)
exit 0
;;
*)
echo "{{ RED }}Incorrect input. Please use only y or n.{{ NORMAL }}"
;;
esac
done
git switch --create "{{ branch_name }}"
_upgrade-rust-fixes-reminder:
#!/usr/bin/env -S bash -euo pipefail
echo -e "\n {{ YELLOW }}Don't forget to fix linting (just lint) and compilation (just check) warnings and errors!{{ NORMAL }}\n"
[confirm('Would you like to commit changes? [y/n]')]
_upgrade-rust-git-commit old_versions new_version file_paths file_names:
#!/usr/bin/env -S bash -euo pipefail
echo "Commiting changes ..."; \
git add {{ file_paths }}; \
sed -r 's/^ {4}//' <<'MSG' | git commit --file -
Upgrade Rust toolchain: {{ old_versions }} -> {{ new_version }}
If you don't have toolchain installed and you use rustup run:
$ rustup toolchain install --profile default --component rustfmt,clippy {{ new_version }}
NOTE: It might be that you have {{ new_version }}.0 installed as stable toolchain, in
that case you still have to run the above command to install exactly {{ new_version }}.
Command: `just upgrade-rust`
Changes:
- Upgrade version of used toolchain in the following places:
{{ ' - ' + replace_regex(file_names, '\s+', "\n - ") }}
MSG
_upgrade-rust version file_path file_name current_version_recipe version_error_msg_part success_msg_part substitude_cmd:
#!/usr/bin/env -S bash -euo pipefail
readonly OLD=$(just {{ current_version_recipe }} {{ file_path }}); \
[ -z "$OLD" ] \
&& ( \
echo "{{ RED }}{{ file_name }}{{ NORMAL }}: Can't determine {{ version_error_msg_part }} before upgrade" >&2; \
exit 1; \
); \
sed -r -i "{{ substitude_cmd }}" {{ file_path }} \
&& ( \
readonly NEW=$(just {{ current_version_recipe }} {{ file_path }}); \
[ -z "NEW" ] \
&& ( \
echo "{{ RED }}{{ file_name }}{{ NORMAL }}: Can't determine {{ version_error_msg_part }} after upgrade" >&2; \
exit 1; \
); \
echo "{{ GREEN }}{{ file_name }}{{ NORMAL }}: {{ success_msg_part }}: {{ BOLD }}$OLD{{ NORMAL }} -> {{ BOLD }}$NEW{{ NORMAL }}"; \
) \
|| echo "{{ RED }}{{ file_name }}{{ NORMAL }}: Upgrade failed" >&2
# Upgrade GitLab CI's Rust to a given version
_upgrade-rust-ci version: (_upgrade-rust version GITLAB_CI_FILE_PATH GITLAB_CI_FILE_NAME '_current-ci-image' 'CI image version' 'image upgraded' 's|image:\s+\"rust:(' + SED_RUST_VERSION_REGEX + ')-([a-z]+)\"\s*$|image: \"rust:' + version + '-\3\"|')
# Upgrade current Rust version in Cargo.toml
_upgrade-rust-cargo version: (_upgrade-rust version CARGO_FILE_PATH CARGO_FILE_NAME '_current-cargo-rust-version' 'Rust version in ' + CARGO_FILE_NAME 'version upgraded' 's|rust-version\s*=\s*\"(' + SED_RUST_VERSION_REGEX + ')\"|rust-version = \"' + version + '\"|')
# Upgrade current Rust version in clippy.toml
_upgrade-rust-clippy version: (_upgrade-rust version CLIPPY_FILE_PATH CLIPPY_FILE_NAME '_current-clippy-rust-version' 'Rust version in ' + CLIPPY_FILE_NAME 'version upgraded' 's|msrv\s*=\s*\"(' + SED_RUST_VERSION_REGEX + ')\"|msrv = \"' + version + '\"|')
# Upgrade current Rust version in rust-toolchain.toml
_upgrade-rust-toolchain version: (_upgrade-rust version TOOLCHAIN_FILE_PATH TOOLCHAIN_FILE_NAME '_current-toolchain-rust-version' 'Rust version in ' + CLIPPY_FILE_NAME 'version upgraded' 's|channel\s*=\s*\"(' + SED_RUST_VERSION_REGEX + ')\"|channel = \"' + version + '\"|')
# Get version of current stable Rust
#
# Parsed string example: "1.90.0 (1159e78c4 2025-09-14)"
# Parsed components:
# \1 - MAJOR
# \2 - MINOR
# \3 - PATCH
# \4 - HASH
# \5 - RELEASE DATE
# \6 - RELEASE YEAR
# \7 - RELEASE MONTH
# \8 - RELEASE DAY
#
# Example of custom format: just rust-stable-version '\5 \4 \1.\2'
# Ouputs: 2025-09-14 1159e78c4 1.90
_rust-stable-version format=DEFAULT_RUST_STABLE_VERSION_FORMAT:
#!/usr/bin/env -S bash -euo pipefail
curl -s {{ RUST_MANIFEST_STABLE_TOML_URL }} \
| tq {{ RUST_MANIFEST_STABLE_VERSION_QUERY }} \
| sed -rn 's|{{ RUST_MANIFEST_STABLE_VERSION_PARSE_REGEX }}|{{ format }}|p'
# Extract current CI image in use
_current-ci-image file_path=GITLAB_CI_FILE_PATH:
#!/usr/bin/env -S bash -euo pipefail
sed -rn "s|\s*image:\s+\"(rust:({{ SED_RUST_VERSION_REGEX }})-([a-z]+))\"\s*$|\1|p" "{{ file_path }}"
_current-ci-rust-version file_path=GITLAB_CI_FILE_PATH:
#!/usr/bin/env -S bash -euo pipefail
sed -rn "s|\s*image:\s+\"rust:({{ SED_RUST_VERSION_REGEX }})-([a-z]+)\"\s*$|\1|p" "{{ file_path }}"
# Extract current Rust version from Cargo.toml
_current-cargo-rust-version file_path=CARGO_FILE_PATH:
#!/usr/bin/env -S bash -euo pipefail
tq --file "{{ file_path }}" --raw {{ CARGO_RUST_VERSION_QUERY }}
# Extract current Rust version from clippy.toml
_current-clippy-rust-version file_path=CLIPPY_FILE_PATH:
#!/usr/bin/env -S bash -euo pipefail
tq --file "{{ file_path }}" --raw {{ CLIPPY_RUST_VERSION_QUERY }}
# Extract current Rust version from rust-toolchain.toml
_current-toolchain-rust-version file_path=TOOLCHAIN_FILE_PATH:
#!/usr/bin/env -S bash -euo pipefail
tq --file "{{ file_path }}" --raw {{ TOOLCHAIN_RUST_VERSION_QUERY }}

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env zsh
#!/usr/bin/env -S zsh -euo pipefail
for i in templates-untranslated/**/*.hbs; do
local template=${${i#templates-untranslated/}}

View File

@@ -1,52 +0,0 @@
geo $allowlist {
default 0;
# CIDR in the list below are using a more lenient limiter
1.2.3.4/32 1;
}
map $allowlist $limit {
0 $binary_remote_addr;
1 "";
}
map $allowlist $limit_loose {
1 $binary_remote_addr;
0 "";
}
# allow 6 requests per min -> one each 10s on avg.
limit_req_zone $limit zone=search_email:10m rate=1r/s;
limit_req_zone $limit_loose zone=search_email_loose:10m rate=1r/m;
limit_req_zone $limit zone=search_fpr_keyid:10m rate=5r/s;
proxy_cache_path /tmp/nginx_cache use_temp_path=off keys_zone=static_cache:10m;
proxy_cache_valid 200 5m;
server {
listen [::]:443 ssl ipv6only=on; # managed by Certbot
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/edge.keys.openpgp.org/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/edge.keys.openpgp.org/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
rewrite_log on;
error_log /home/hagrid/error.log notice;
root /home/hagrid/run/public;
server_name edge.keys.openpgp.org; # managed by Certbot
include hagrid-routes.conf;
}
server {
if ($host = edge.keys.openpgp.org) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80 ;
listen [::]:80 ;
server_name edge.keys.openpgp.org;
return 404; # managed by Certbot
}

View File

@@ -1,10 +1,28 @@
error_log stderr;
pid nginx/nginx.pid;
pid nginx.pid;
daemon off;
http {
# allow 6 requests per min -> one each 10s on avg.
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=6r/m;
geo $allowlist {
default 0;
# CIDR in the list below are using a more lenient limiter
1.2.3.4/32 1;
}
map $allowlist $limit {
0 $binary_remote_addr;
1 "";
}
map $allowlist $limit_loose {
1 $binary_remote_addr;
0 "";
}
# limit zones are used in hagrid-routes.conf
limit_req_zone $limit zone=search_email:10m rate=1r/s;
limit_req_zone $limit_loose zone=search_email_loose:10m rate=1r/m;
limit_req_zone $binary_remote_addr zone=search_fpr_keyid:10m rate=5r/s;
proxy_cache_path /tmp/nginx_cache use_temp_path=off keys_zone=static_cache:10m;
proxy_cache_valid 200 5m;
@@ -12,13 +30,13 @@ http {
server {
listen 0.0.0.0:8090;
access_log nginx/access_log;
access_log stderr;
# To debug the rewrite rules, enable these directives:
#error_log stderr notice;
#rewrite_log on;
# error_log stderr notice;
# rewrite_log on;
include /etc/nginx/mime.types;
# include /etc/nginx/mime.types;
default_type application/octet-stream;
root dist/public;

View File

@@ -17,57 +17,21 @@ msgstr ""
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 "
"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n"
#: src/mail.rs:107
msgctxt "Subject for verification email"
msgid "Verify {userid} for your key on {domain}"
msgstr "‫تحققْ من {userid} لأجل مفتاحك في {domain}"
#: src/mail.rs:140
msgctxt "Subject for manage email"
msgid "Manage your key on {domain}"
msgstr "‫أدرْ مفتاحك في {domain}"
#: src/i18n_helpers.rs:8
msgid "No key found for fingerprint {fingerprint}"
msgstr "‫لم يعثر على المفتاح ذي البصمة {fingerprint}"
#: src/i18n_helpers.rs:10
msgid "No key found for key id {key_id}"
msgstr "‫لم يعثر على المفتاح ذي المُعرِّف {key_id}"
#: src/i18n_helpers.rs:12
msgid "No key found for email address {email}"
msgstr "‫لم يعثر على المفتاح ذي العنوان الإلكتروني {email}"
#: src/i18n_helpers.rs:13
msgid "Search by Short Key ID is not supported."
msgstr "‫إن البحث بالمُعرِّف المختصر للمفتاح ليس مدعوما."
#: src/i18n_helpers.rs:14
msgid "Invalid search query."
msgstr "طلب البحث غير صالح."
#: src/gettext_strings.rs:4
msgid "Error"
msgstr "هناك خطأ"
#: src/gettext_strings.rs:5
msgid "Looks like something went wrong :("
msgstr "‫يبدو أن مشكلة ما حصلت :("
#: src/gettext_strings.rs:6
msgid "Error message: {{ internal_error }}"
msgstr "‫رسالة الخطأ : {{ internal_error }}"
#: src/gettext_strings.rs:7
msgid "There was an error with your request:"
msgstr "‫هناك خطأ في طلبك :"
#: src/gettext_strings.rs:8
msgid "We found an entry for <span class=\"email\">{{ query }}</span>:"
msgstr "‫لقد عثرنا على مُدخَلة لـ <span class=\"email\">{{ query }}</span> :"
#: src/gettext_strings.rs:9
msgid ""
"<strong>Hint:</strong> It's more convenient to use <span class=\"brand"
"\">keys.openpgp.org</span> from your OpenPGP software.<br /> Take a look at "
@@ -77,19 +41,15 @@ msgstr ""
"org</span> انطلاقا من برنامج OpenPGP. <br /> عليك بإلقاء نظرة عن ذلك في <a "
"href=\"/about/usage\">دليل الاستخدام</a> للمزيد من التفاصيل."
#: src/gettext_strings.rs:10
msgid "debug info"
msgstr "معلومات التصحيح"
#: src/gettext_strings.rs:11
msgid "Search by Email Address / Key ID / Fingerprint"
msgstr "ابحث بعنوان البريد الإلكتروني أو بمُعرِّف المفتاح أو بالبصمة"
#: src/gettext_strings.rs:12
msgid "Search"
msgstr "ابحث"
#: src/gettext_strings.rs:13
msgid ""
"You can also <a href=\"/upload\">upload</a> or <a href=\"/manage\">manage</"
"a> your key."
@@ -97,15 +57,12 @@ msgstr ""
"يمكنك أيضا <a href=\"/upload\">رفع</a> أو <a href=\"/manage\">إدارة</a> "
"مفتاحك."
#: src/gettext_strings.rs:14
msgid "Find out more <a href=\"/about\">about this service</a>."
msgstr "اعرف المزيد <a href=\"/about\">عن هذه الخدمة</a>."
#: src/gettext_strings.rs:15
msgid "News:"
msgstr "اﻷخبار :"
#: src/gettext_strings.rs:16
msgid ""
"<a href=\"/about/news#2019-11-12-celebrating-100k\">Celebrating 100.000 "
"verified addresses! 📈</a> (2019-11-12)"
@@ -113,15 +70,12 @@ msgstr ""
"<a href=\"/about/news#2019-11-12-celebrating-100k\">‫احتفلنا بالتحقق من 100 "
"000 عنوان للبريد الإلكتروني ! 📈</a> (2019-11-12)"
#: src/gettext_strings.rs:17
msgid "v{{ version }} built from"
msgstr "‫الإصدار {{ version }} بُنِي انطلاقا من"
#: src/gettext_strings.rs:18
msgid "Powered by <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>"
msgstr "‫مُشغَّل بواسطة <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>"
#: src/gettext_strings.rs:19
msgid ""
"Background image retrieved from <a href=\"https://www.toptal.com/designers/"
"subtlepatterns/subtle-grey/\">Subtle Patterns</a> under CC BY-SA 3.0"
@@ -130,23 +84,18 @@ msgstr ""
"subtlepatterns/subtle-grey/\">Subtle Patterns</a> بموجب اﻹصدار 3.0 من « رخصة "
"المشاع الإبداعي » (النِّسبَة، الترخيص بالمثل)"
#: src/gettext_strings.rs:20
msgid "Maintenance Mode"
msgstr "وضع الصيانة"
#: src/gettext_strings.rs:21
msgid "Manage your key"
msgstr "أدِر مفتاحك"
#: src/gettext_strings.rs:22
msgid "Enter any verified email address for your key"
msgstr "أدخِل عنوان البريد الإلكتروني المُتحقَّق منه لمفتاحك"
#: src/gettext_strings.rs:23
msgid "Send link"
msgstr "أرسِلْ الوصلة"
#: src/gettext_strings.rs:24
msgid ""
"We will send you an email with a link you can use to remove any of your "
"email addresses from search."
@@ -154,7 +103,6 @@ msgstr ""
"سنرسل لك رسالة إلكترونية تحتوي على وصلة تسمح لك بإزالة أحد عناوين بريدك "
"اﻹلكتروني من البحث."
#: src/gettext_strings.rs:25
msgid ""
"Managing the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
@@ -162,15 +110,12 @@ msgstr ""
"‫إدارة المفتاح <span class=\"fingerprint\"><a href=\"{{ key_link }}\" target="
"\"_blank\">{{ key_fpr }}</a></span>."
#: src/gettext_strings.rs:26
msgid "Your key is published with the following identity information:"
msgstr "مفتاحك منشور بموجب معلومات الهوية أسفله :"
#: src/gettext_strings.rs:27
msgid "Delete"
msgstr "احدف"
#: src/gettext_strings.rs:28
msgid ""
"Clicking \"delete\" on any address will remove it from this key. It will no "
"longer appear in a search.<br /> To add another address, <a href=\"/upload"
@@ -180,7 +125,6 @@ msgstr ""
"يظهر بعد ذلك في البحث. <br /> لإضافة عنوان آخر، عليك ب<a href=\"/upload"
"\">رفع</a> المفتاح مرة أخرى."
#: src/gettext_strings.rs:29
msgid ""
"Your key is published as only non-identity information. (<a href=\"/about\" "
"target=\"_blank\">What does this mean?</a>)"
@@ -188,11 +132,9 @@ msgstr ""
"‫لقد نُشِر مفتاحك فقط كمعلومة لا تمكِّن من التعرف على هويتك. (<a href=\"/about\" "
"target=\"_blank\">ماذا يعني ذلك ؟</a>)"
#: src/gettext_strings.rs:30
msgid "To add an address, <a href=\"/upload\">upload</a> the key again."
msgstr "ﻹضافة عنوان، عليك ب<a href=\"/upload\">رفع</a> المفتاح مرة أخرى."
#: src/gettext_strings.rs:31
msgid ""
"We have sent an email with further instructions to <span class=\"email"
"\">{{ address }}</span>."
@@ -200,11 +142,9 @@ msgstr ""
"‫لقد أرسلنا رسالة إليك، فيها تعليمات أخرى إلى العنوان اﻹلكتروني <span class="
"\"email\">{{ address }}</span>."
#: src/gettext_strings.rs:32
msgid "This address has already been verified."
msgstr "لقد سبق التحقق من هذا العنوان."
#: src/gettext_strings.rs:33
msgid ""
"Your key <span class=\"fingerprint\">{{ key_fpr }}</span> is now published "
"for the identity <a href=\"{{userid_link}}\" target=\"_blank\"><span class="
@@ -214,15 +154,12 @@ msgstr ""
"للهوية <a href=\"{{userid_link}}\" target=\"_blank\"><span class=\"email"
"\">{{ userid }}</span></a>."
#: src/gettext_strings.rs:34
msgid "Upload your key"
msgstr "ارفع مفتاحك"
#: src/gettext_strings.rs:35
msgid "Upload"
msgstr "ارفع"
#: src/gettext_strings.rs:36
msgid ""
"Need more info? Check our <a target=\"_blank\" href=\"/about\">intro</a> and "
"<a target=\"_blank\" href=\"/about/usage\">usage guide</a>."
@@ -230,7 +167,6 @@ msgstr ""
"‫أأنت بحاجة إلى معلومات أخرى ؟ يمكنك تفقد <a target=\"_blank\" href=\"/about"
"\">مقدمتنا</a> و<a target=\"_blank\" href=\"/about/usage\">دليلنا</a>."
#: src/gettext_strings.rs:37
msgid ""
"You uploaded the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
@@ -238,11 +174,9 @@ msgstr ""
"‫لقد رفعت المفتاح <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
#: src/gettext_strings.rs:38
msgid "This key is revoked."
msgstr "لقد أُبطِل هذا المفتاح."
#: src/gettext_strings.rs:39
msgid ""
"It is published without identity information and can't be made available for "
"search by email address (<a href=\"/about\" target=\"_blank\">what does this "
@@ -252,7 +186,6 @@ msgstr ""
"بواسطة عنوان البريد الإلكتروني (<a href=\"/about\" target=\"_blank\">ماذا "
"يعني ذلك ؟</a>)."
#: src/gettext_strings.rs:40
msgid ""
"This key is now published with the following identity information (<a href="
"\"/about\" target=\"_blank\">what does this mean?</a>):"
@@ -260,11 +193,9 @@ msgstr ""
"مفتاحك منشور بموجب معلومات الهوية أسفله (<a href=\"/about\" target=\"_blank"
"\">ماذا يعني ذلك ؟</a>) :"
#: src/gettext_strings.rs:41
msgid "Published"
msgstr "نُشِر"
#: src/gettext_strings.rs:42
msgid ""
"This key is now published with only non-identity information. (<a href=\"/"
"about\" target=\"_blank\">What does this mean?</a>)"
@@ -272,18 +203,15 @@ msgstr ""
"‫لقد نُشِر مفتاحك الآن فقط كمعلومة لا تمكِّن من التعرف على هويتك. (<a href=\"/"
"about\" target=\"_blank\">ماذا يعني ذلك ؟</a>)"
#: src/gettext_strings.rs:43
msgid ""
"To make the key available for search by email address, you can verify it "
"belongs to you:"
msgstr ""
"لتمكين البحث عن المفتاح عبر عنوان البريد اﻹلكتروني، يمكنك تأكيد ملكيتك له :"
#: src/gettext_strings.rs:44
msgid "Verification Pending"
msgstr "في انتظار التحقُّق"
#: src/gettext_strings.rs:45
msgid ""
"<strong>Note:</strong> Some providers delay emails for up to 15 minutes to "
"prevent spam. Please be patient."
@@ -291,11 +219,9 @@ msgstr ""
"<strong>ملاحظة : </strong>قد يؤخر بعض مزودي خدمة البريد اﻹلكتروني وصول "
"الرسائل إلى ما يقارب 15 دقيقة للوقاية من الرسائل المزعجة. يُرجى الصبر قليلا."
#: src/gettext_strings.rs:46
msgid "Send Verification Email"
msgstr "أرسِل رسالة التحقُّق"
#: src/gettext_strings.rs:47
msgid ""
"This key contains one identity that could not be parsed as an email address."
"<br /> This identity can't be published on <span class=\"brand\">keys."
@@ -306,7 +232,6 @@ msgstr ""
"<br /> لذا، سيتعذر نشر هذه الهوية في <span class=\"brand\">keys.openpgp.org</"
"span>. (<a href=\"/about/faq#non-email-uids\" target=\"_blank\">لماذا ؟</a>)"
#: src/gettext_strings.rs:48
msgid ""
"This key contains {{ count_unparsed }} identities that could not be parsed "
"as an email address.<br /> These identities can't be published on <span "
@@ -318,7 +243,6 @@ msgstr ""
"\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-email-uids\" target="
"\"_blank\">لماذا ؟</a>)"
#: src/gettext_strings.rs:49
msgid ""
"This key contains one revoked identity, which is not published. (<a href=\"/"
"about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
@@ -326,7 +250,6 @@ msgstr ""
"‫يحتوي هذا المفتاح على هوية واحدة قد أُبطلَت، ولم يتم نشرها. (<a href=\"/about/"
"faq#non-email-uids\" target=\"_blank\">لماذا ؟</a>)"
#: src/gettext_strings.rs:50
msgid ""
"This key contains {{ count_revoked }} revoked identities, which are not "
"published. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
@@ -334,11 +257,9 @@ msgstr ""
"‫يحتوي هذا المفتاح على {{ count_revoked }} هويات قد أُبطلَت، ولم يتم نشرها. (<a "
"href=\"/about/faq#non-email-uids\" target=\"_blank\">لماذا ؟</a>)"
#: src/gettext_strings.rs:51
msgid "Your keys have been successfully uploaded:"
msgstr "لقد رُفعَت مفاتيحك بنجاح :"
#: src/gettext_strings.rs:52
msgid ""
"<strong>Note:</strong> To make keys searchable by email address, you must "
"upload them individually."
@@ -346,11 +267,9 @@ msgstr ""
"<strong>ملاحظة :</strong> لتمكين البحث عن مفاتيحك بواسطة عنوان البريد "
"الإلكتروني، عليك برفعها فرديا. "
#: src/gettext_strings.rs:53
msgid "Verifying your email address…"
msgstr "يجري التحقق من عنوان بريدك الإلكتروني…‏"
#: src/gettext_strings.rs:54
msgid ""
"If the process doesn't complete after a few seconds, please <input type="
"\"submit\" class=\"textbutton\" value=\"click here\" />."
@@ -358,15 +277,12 @@ msgstr ""
"إذا لم تكتمل العملية بعد بضع ثوان، يُرجى <input type=\"submit\" class="
"\"textbutton\" value=\"cliquer ici\"/>."
#: src/gettext_strings.rs:56
msgid "Manage your key on {{domain}}"
msgstr "أدرْ مفتاحك في {domain}"
#: src/gettext_strings.rs:58
msgid "Hi,"
msgstr "أهلا بك،‏"
#: src/gettext_strings.rs:59
msgid ""
"This is an automated message from <a href=\"{{base_uri}}\" style=\"text-"
"decoration:none; color: #333\">{{domain}}</a>."
@@ -374,22 +290,18 @@ msgstr ""
"‫هذه رسالة آلية من <a href=\"{{base_uri}}\" style=\"text-decoration:none; "
"color: #333\">{{domain}}</a>."
#: src/gettext_strings.rs:60
msgid "If you didn't request this message, please ignore it."
msgstr "إذا لم تطلب التوصل بهذه الرسالة، يُرجى تجاهلها."
#: src/gettext_strings.rs:61
msgid "OpenPGP key: <tt>{{primary_fp}}</tt>"
msgstr "‫مفتاح OpenPGP : <tt>{{primary_fp}}</tt>"
#: src/gettext_strings.rs:62
msgid ""
"To manage and delete listed addresses on this key, please follow the link "
"below:"
msgstr ""
"لإدارة أو حذف العناوين المُدرَجة مع هذا المفتاح، يُرجى اتباع الوصلة أسفله :"
#: src/gettext_strings.rs:63
msgid ""
"You can find more info at <a href=\"{{base_uri}}/about\">{{domain}}/about</"
"a>."
@@ -397,27 +309,21 @@ msgstr ""
"‫يمكنك الاطلاع على المزيد من المعلومات عبر الصفحة <a href=\"{{base_uri}}/about"
"\">{{domain}}/about</a>."
#: src/gettext_strings.rs:64
msgid "distributing OpenPGP keys since 2019"
msgstr "‫يوزِّع مفاتيح OpenPGP منذ 2019"
#: src/gettext_strings.rs:67
msgid "This is an automated message from {{domain}}."
msgstr "‫هذه رسالة آلية من {{domain}}."
#: src/gettext_strings.rs:69
msgid "OpenPGP key: {{primary_fp}}"
msgstr "‫مفتاح OpenPGP : {{primary_fp}}"
#: src/gettext_strings.rs:71
msgid "You can find more info at {{base_uri}}/about"
msgstr "‫يمكنك الاطلاع على المزيد من المعلومات عبر الصفحة {{domain}}/about."
#: src/gettext_strings.rs:74
msgid "Verify {{userid}} for your key on {{domain}}"
msgstr "‫تحققْ من {userid} لأجل مفتاحك في {domain}"
#: src/gettext_strings.rs:80
msgid ""
"To let others find this key from your email address \"<a rel=\"nofollow\" "
"href=\"#\" style=\"text-decoration:none; color: #333\">{{userid}}</a>\", "
@@ -427,7 +333,6 @@ msgstr ""
"<a rel=\"nofollow\" href=\"#\" style=\"text-decoration:none; color : "
"#333\">{userid}}</a> »، يُرجى الضغط على الوصلة أسفله :"
#: src/gettext_strings.rs:88
msgid ""
"To let others find this key from your email address \"{{userid}}\",\n"
"please follow the link below:"
@@ -436,42 +341,57 @@ msgstr ""
"« {userid}} »،‬\n"
"‫يُرجى اتباع الوصلة أسفله :"
#: src/web/manage.rs:103
msgid "No key found for fingerprint {}"
msgstr "‫لم يعثر على المفتاح ذي البصمة {}"
msgid "No key found for key id {}"
msgstr "‫لم يعثر على المفتاح ذي المُعرِّف {}"
msgid "No key found for email address {}"
msgstr "‫لم يعثر على المفتاح ذي العنوان الإلكتروني {}"
msgid "Search by Short Key ID is not supported."
msgstr "‫إن البحث بالمُعرِّف المختصر للمفتاح ليس مدعوما."
msgid "Invalid search query."
msgstr "طلب البحث غير صالح."
msgctxt "Subject for verification email, {0} = userid, {1} = keyserver domain"
msgid "Verify {0} for your key on {1}"
msgstr "‫تحققْ من {0} لأجل مفتاحك في {1}"
msgctxt "Subject for manage email, {} = keyserver domain"
msgid "Manage your key on {}"
msgstr "‫أدرْ مفتاحك في {}"
msgid "This link is invalid or expired"
msgstr "هذه الوصلة باطلة أو انتهت صلاحيتها"
#: src/web/manage.rs:129
msgid "Malformed address: {address}"
msgstr "‫العنوان غير صحيح : {address}"
#, fuzzy
msgid "Malformed address: {}"
msgstr "‫العنوان غير صحيح : {}"
#: src/web/manage.rs:136
msgid "No key for address: {address}"
msgstr "‫لا يوجد مفتاح لهذا العنوان : {address}"
#, fuzzy
msgid "No key for address: {}"
msgstr "‫لا يوجد مفتاح لهذا العنوان : {}"
#: src/web/manage.rs:152
msgid "A request has already been sent for this address recently."
msgstr "لقد سبق إرسال الطلب إلى هذا العنوان حديثا."
#: src/web/vks.rs:111
msgid "Parsing of key data failed."
msgstr "فشِل تحليل بيانات المفتاح."
#: src/web/vks.rs:120
msgid "Whoops, please don't upload secret keys!"
msgstr "‫تحذير : يُرجى عدم رفع مفاتيحك السرية !"
#: src/web/vks.rs:133
msgid "No key uploaded."
msgstr "لم يُرفَع أي مفتاح."
#: src/web/vks.rs:177
msgid "Error processing uploaded key."
msgstr "حدث خطأ أثناء معالجة المفتاح المرفوع."
#: src/web/vks.rs:247
msgid "Upload session expired. Please try again."
msgstr "انتهت مهلة جلسة الرفع. يُرجى المحاولة مرة أخرى."
#: src/web/vks.rs:284
msgid "Invalid verification link."
msgstr "وصلة التحقُّق باطلة."

View File

@@ -1,8 +1,8 @@
#
#
# Translators:
# Vincent Breitmoser <look@my.amazin.horse>, 2021
# Kevin Kandlbinder <kevin@kevink.dev>, 2021
#
#
msgid ""
msgstr ""
"Project-Id-Version: hagrid\n"
@@ -11,100 +11,58 @@ msgstr ""
"PO-Revision-Date: 2019-09-27 18:05+0000\n"
"Last-Translator: Kevin Kandlbinder <kevin@kevink.dev>, 2021\n"
"Language-Team: German (https://www.transifex.com/otf/teams/102430/de/)\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: de\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: src/mail.rs:107
msgctxt "Subject for verification email"
msgid "Verify {userid} for your key on {domain}"
msgstr "Bestätige {userid} für deinen Schlüssel auf {domain}"
#: src/mail.rs:140
msgctxt "Subject for manage email"
msgid "Manage your key on {domain}"
msgstr "Schlüssel-Verwaltung auf {domain}"
#: src/i18n_helpers.rs:8
msgid "No key found for fingerprint {fingerprint}"
msgstr "Kein Schlüssel gefunden für Fingerprint {fingerprint}"
#: src/i18n_helpers.rs:10
msgid "No key found for key id {key_id}"
msgstr "Kein Schlüssel gefunden für Schlüssel-Id {key_id}"
#: src/i18n_helpers.rs:12
msgid "No key found for email address {email}"
msgstr "Kein Schlüssel gefunden für Email-Adresse {email}"
#: src/i18n_helpers.rs:13
msgid "Search by Short Key ID is not supported."
msgstr "Suche nach kurzer Schlüssel-ID wird nicht unterstützt."
#: src/i18n_helpers.rs:14
msgid "Invalid search query."
msgstr "Ungültige Suchanfrage."
#: src/gettext_strings.rs:4
msgid "Error"
msgstr "Fehler"
#: src/gettext_strings.rs:5
msgid "Looks like something went wrong :("
msgstr "Die Operation hat einen internen Fehler versursacht :("
#: src/gettext_strings.rs:6
msgid "Error message: {{ internal_error }}"
msgstr "<strong>Fehlermeldung:</strong> {{ internal_error }}"
#: src/gettext_strings.rs:7
msgid "There was an error with your request:"
msgstr "Die Anfrage verursachte einen Fehler:"
#: src/gettext_strings.rs:8
msgid "We found an entry for <span class=\"email\">{{ query }}</span>:"
msgstr "Eintrag gefunden für <span class=\"email\">{{ query }}</span>:"
#: src/gettext_strings.rs:9
msgid ""
"<strong>Hint:</strong> It's more convenient to use <span "
"class=\"brand\">keys.openpgp.org</span> from your OpenPGP software.<br /> "
"Take a look at our <a href=\"/about/usage\">usage guide</a> for details."
"<strong>Hint:</strong> It's more convenient to use <span class=\"brand"
"\">keys.openpgp.org</span> from your OpenPGP software.<br /> Take a look at "
"our <a href=\"/about/usage\">usage guide</a> for details."
msgstr ""
"<strong>Tip:</strong> Es ist bequemer, <span class=\"brand\">keys.openpgp.org</span> aus OpenPGP-Software heraus zu verwenden. <br />\n"
"<strong>Tip:</strong> Es ist bequemer, <span class=\"brand\">keys.openpgp."
"org</span> aus OpenPGP-Software heraus zu verwenden. <br />\n"
"Mehr dazu findest du in den <a href=\"/about/usage\">Nutzungshinweisen</a>."
#: src/gettext_strings.rs:10
msgid "debug info"
msgstr "debug info"
#: src/gettext_strings.rs:11
msgid "Search by Email Address / Key ID / Fingerprint"
msgstr "Suche nach Email-Adresse / Schlüssel-ID / Fingerprint"
#: src/gettext_strings.rs:12
msgid "Search"
msgstr "Suchen"
#: src/gettext_strings.rs:13
msgid ""
"You can also <a href=\"/upload\">upload</a> or <a "
"href=\"/manage\">manage</a> your key."
"You can also <a href=\"/upload\">upload</a> or <a href=\"/manage\">manage</"
"a> your key."
msgstr ""
"Du kannst deinen Schlüssel <a href=\"/upload\">hochladen</a> oder <a "
"href=\"/manage\">verwalten</a>."
"Du kannst deinen Schlüssel <a href=\"/upload\">hochladen</a> oder <a href=\"/"
"manage\">verwalten</a>."
#: src/gettext_strings.rs:14
msgid "Find out more <a href=\"/about\">about this service</a>."
msgstr "Erfahre mehr <a href=\"/about\">über diesen Keyserver</a>."
#: src/gettext_strings.rs:15
msgid "News:"
msgstr "News:"
#: src/gettext_strings.rs:16
msgid ""
"<a href=\"/about/news#2019-11-12-celebrating-100k\">Celebrating 100.000 "
"verified addresses! 📈</a> (2019-11-12)"
@@ -112,40 +70,31 @@ msgstr ""
"<a href=\"/about/news#2019-11-12-celebrating-100k\">Wir feiern 100.000 "
"überprüfte Adressen! 📈</a> (2019-11-12)"
#: src/gettext_strings.rs:17
msgid "v{{ version }} built from"
msgstr "v{{ version }}, Revision"
#: src/gettext_strings.rs:18
msgid "Powered by <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>"
msgstr "Powered by <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>"
#: src/gettext_strings.rs:19
msgid ""
"Background image retrieved from <a "
"href=\"https://www.toptal.com/designers/subtlepatterns/subtle-grey/\">Subtle"
" Patterns</a> under CC BY-SA 3.0"
"Background image retrieved from <a href=\"https://www.toptal.com/designers/"
"subtlepatterns/subtle-grey/\">Subtle Patterns</a> under CC BY-SA 3.0"
msgstr ""
"Hintergrund von <a href=\"https://www.toptal.com/designers/subtlepatterns"
"/subtle-grey/\">Subtle Patterns</a> unter CC BY-SA 3.0"
"Hintergrund von <a href=\"https://www.toptal.com/designers/subtlepatterns/"
"subtle-grey/\">Subtle Patterns</a> unter CC BY-SA 3.0"
#: src/gettext_strings.rs:20
msgid "Maintenance Mode"
msgstr "Wartungsarbeiten"
#: src/gettext_strings.rs:21
msgid "Manage your key"
msgstr "Schlüssel verwalten"
#: src/gettext_strings.rs:22
msgid "Enter any verified email address for your key"
msgstr "Email-Adresse des zu verwaltenden Schlüssels"
#: src/gettext_strings.rs:23
msgid "Send link"
msgstr "Sende Link"
#: src/gettext_strings.rs:24
msgid ""
"We will send you an email with a link you can use to remove any of your "
"email addresses from search."
@@ -153,140 +102,119 @@ msgstr ""
"Du wirst eine Email mit einem Link erhalten, der es erlaubt, Adressen aus "
"der Suche zu entfernen."
#: src/gettext_strings.rs:25
msgid ""
"Managing the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
msgstr ""
"Verwaltung des Schlüssels <span class=\"fingerprint\"><a href=\"{{ key_link "
"}}\" target=\"_blank\">{{ key_fpr }}</a></span>."
"Verwaltung des Schlüssels <span class=\"fingerprint\"><a href="
"\"{{ key_link }}\" target=\"_blank\">{{ key_fpr }}</a></span>."
#: src/gettext_strings.rs:26
msgid "Your key is published with the following identity information:"
msgstr "Dieser Schlüssel ist veröffentlicht mit diesen Identitäten:"
#: src/gettext_strings.rs:27
msgid "Delete"
msgstr "Entfernen"
#: src/gettext_strings.rs:28
msgid ""
"Clicking \"delete\" on any address will remove it from this key. It will no "
"longer appear in a search.<br /> To add another address, <a "
"href=\"/upload\">upload</a> the key again."
"longer appear in a search.<br /> To add another address, <a href=\"/upload"
"\">upload</a> the key again."
msgstr ""
"Veröffentlichte Adressen können hier aus dem Schlüssel und der Suche "
"entfernt werden. <br /> Um eine Adresse hinzuzufügen, muss der Schlüssel <a "
"href=\"/upload\">erneut hochgeladen</a> werden."
#: src/gettext_strings.rs:29
msgid ""
"Your key is published as only non-identity information. (<a href=\"/about\""
" target=\"_blank\">What does this mean?</a>)"
"Your key is published as only non-identity information. (<a href=\"/about\" "
"target=\"_blank\">What does this mean?</a>)"
msgstr ""
"Dieser Schlüssel ist jetzt ohne Identitäts-Informationen veröffentlicht. (<a"
" href=\"/about\" target=\"_blank\">Was heisst das?</a>)"
"Dieser Schlüssel ist jetzt ohne Identitäts-Informationen veröffentlicht. (<a "
"href=\"/about\" target=\"_blank\">Was heisst das?</a>)"
#: src/gettext_strings.rs:30
msgid "To add an address, <a href=\"/upload\">upload</a> the key again."
msgstr ""
"Um eine Identität hinzuzufügen, lade den Schlüssel <a "
"href=\"/upload\">erneut hoch</a>."
"Um eine Identität hinzuzufügen, lade den Schlüssel <a href=\"/upload"
"\">erneut hoch</a>."
#: src/gettext_strings.rs:31
msgid ""
"We have sent an email with further instructions to <span class=\"email\">{{ "
"address }}</span>."
"We have sent an email with further instructions to <span class=\"email"
"\">{{ address }}</span>."
msgstr ""
"Eine Email mit den nächsten Schritten wurde an <span class=\"email\">{{ "
"address }}</span> gesendet."
"Eine Email mit den nächsten Schritten wurde an <span class=\"email"
"\">{{ address }}</span> gesendet."
#: src/gettext_strings.rs:32
msgid "This address has already been verified."
msgstr "Diese Adresse war bereits bestätigt."
#: src/gettext_strings.rs:33
msgid ""
"Your key <span class=\"fingerprint\">{{ key_fpr }}</span> is now published "
"for the identity <a href=\"{{userid_link}}\" target=\"_blank\"><span "
"class=\"email\">{{ userid }}</span></a>."
"for the identity <a href=\"{{userid_link}}\" target=\"_blank\"><span class="
"\"email\">{{ userid }}</span></a>."
msgstr ""
"Dein Schlüssel <span class=\"fingerprint\">{{key_fpr}}</span> ist jetzt "
"veröffentlicht mit der Identität <a href=\"{{userid_link}}\" "
"target=\"_blank\"><span class=\"email\">{{ userid }}</span></a>."
"veröffentlicht mit der Identität <a href=\"{{userid_link}}\" target=\"_blank"
"\"><span class=\"email\">{{ userid }}</span></a>."
#: src/gettext_strings.rs:34
msgid "Upload your key"
msgstr "Schlüssel hochladen"
#: src/gettext_strings.rs:35
msgid "Upload"
msgstr "Upload"
#: src/gettext_strings.rs:36
msgid ""
"Need more info? Check our <a target=\"_blank\" href=\"/about\">intro</a> and"
" <a target=\"_blank\" href=\"/about/usage\">usage guide</a>."
"Need more info? Check our <a target=\"_blank\" href=\"/about\">intro</a> and "
"<a target=\"_blank\" href=\"/about/usage\">usage guide</a>."
msgstr ""
"Mehr Info? Wirf einen Blick auf unsere <a target=\"_blank\" "
"href=\"/about\">Übersicht</a> und <a target=\"_blank\" "
"href=\"/about/usage\">Nutzungshinweise</a>."
"Mehr Info? Wirf einen Blick auf unsere <a target=\"_blank\" href=\"/about"
"\">Übersicht</a> und <a target=\"_blank\" href=\"/about/usage"
"\">Nutzungshinweise</a>."
#: src/gettext_strings.rs:37
msgid ""
"You uploaded the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
msgstr ""
"Schlüssel <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{key_fpr}}</a></span> erfolgreich hochgeladen."
"Schlüssel <span class=\"fingerprint\"><a href=\"{{ key_link }}\" target="
"\"_blank\">{{key_fpr}}</a></span> erfolgreich hochgeladen."
#: src/gettext_strings.rs:38
msgid "This key is revoked."
msgstr "Dieser Schlüssel ist widerrufen."
#: src/gettext_strings.rs:39
msgid ""
"It is published without identity information and can't be made available for"
" search by email address (<a href=\"/about\" target=\"_blank\">what does "
"this mean?</a>)."
"It is published without identity information and can't be made available for "
"search by email address (<a href=\"/about\" target=\"_blank\">what does this "
"mean?</a>)."
msgstr ""
"Er wird veröffentlicht ohne Identitäten, ist aber nicht für eine Suche nach "
"Email-Adresse verfügbar. (<a href=\"/about\" target=\"_blank\">Was heisst "
"das?</a>)"
#: src/gettext_strings.rs:40
msgid ""
"This key is now published with the following identity information (<a "
"href=\"/about\" target=\"_blank\">what does this mean?</a>):"
"This key is now published with the following identity information (<a href="
"\"/about\" target=\"_blank\">what does this mean?</a>):"
msgstr ""
"Dieser Schlüssel ist mit folgenden Identitäten veröffentlicht (<a "
"href=\"/about\" target=\"_blank\">was heisst das?</a>):"
"Dieser Schlüssel ist mit folgenden Identitäten veröffentlicht (<a href=\"/"
"about\" target=\"_blank\">was heisst das?</a>):"
#: src/gettext_strings.rs:41
msgid "Published"
msgstr "Veröffentlicht"
#: src/gettext_strings.rs:42
msgid ""
"This key is now published with only non-identity information. (<a "
"href=\"/about\" target=\"_blank\">What does this mean?</a>)"
"This key is now published with only non-identity information. (<a href=\"/"
"about\" target=\"_blank\">What does this mean?</a>)"
msgstr ""
"Dieser Schlüssel ist jetzt ohne Identitäts-Informationen veröffentlicht. (<a"
" href=\"/about\" target=\"_blank\">Was heisst das?</a>)"
"Dieser Schlüssel ist jetzt ohne Identitäts-Informationen veröffentlicht. (<a "
"href=\"/about\" target=\"_blank\">Was heisst das?</a>)"
#: src/gettext_strings.rs:43
msgid ""
"To make the key available for search by email address, you can verify it "
"belongs to you:"
msgstr ""
"Um den Schlüssel für eine Suche nach Email-Adresse verfügbar zu machen, muss"
" die entsprechende Adresse erst verifiziert werden:"
"Um den Schlüssel für eine Suche nach Email-Adresse verfügbar zu machen, muss "
"die entsprechende Adresse erst verifiziert werden:"
#: src/gettext_strings.rs:44
msgid "Verification Pending"
msgstr "Bestätigung wird erwartet"
#: src/gettext_strings.rs:45
msgid ""
"<strong>Note:</strong> Some providers delay emails for up to 15 minutes to "
"prevent spam. Please be patient."
@@ -294,23 +222,20 @@ msgstr ""
"<strong>Hinweis:</strong> Manche Provider verzögern den Empfang von Emails "
"um bis zu 15 Minuten, um Spam zu verhindern. Bitte einen Moment Geduld."
#: src/gettext_strings.rs:46
msgid "Send Verification Email"
msgstr "Bestätigungs-Email senden"
#: src/gettext_strings.rs:47
msgid ""
"This key contains one identity that could not be parsed as an email "
"address.<br /> This identity can't be published on <span "
"class=\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-email-"
"uids\" target=\"_blank\">Why?</a>)"
"This key contains one identity that could not be parsed as an email address."
"<br /> This identity can't be published on <span class=\"brand\">keys."
"openpgp.org</span>. (<a href=\"/about/faq#non-email-uids\" target=\"_blank"
"\">Why?</a>)"
msgstr ""
"Dieser Schlüssel enthält eine Identität welche nicht als Email Adresse "
"interpretiert werden konnte.<br /> Diese Identität kann nicht auf <span "
"class=\"brand\">keys.openpgp.org</span> veröffentlich werden. (<a "
"href=\"/about/faq#non-email-uids\" target=\"_blank\">Warum?</a>)"
"class=\"brand\">keys.openpgp.org</span> veröffentlich werden. (<a href=\"/"
"about/faq#non-email-uids\" target=\"_blank\">Warum?</a>)"
#: src/gettext_strings.rs:48
msgid ""
"This key contains {{ count_unparsed }} identities that could not be parsed "
"as an email address.<br /> These identities can't be published on <span "
@@ -319,32 +244,28 @@ msgid ""
msgstr ""
"Dieser Schlüssel enthält {{ count_unparsed }} Identitäten, die nicht als "
"Email-Adressen erkannt werden konnten.<br /> Diese Identitäten können nicht "
"auf <span class=\"brand\">keys.openpgp.org</span> veröffentlicht werden. (<a"
" href=\"/about/faq#non-email-uids\" target=\"_blank\">Warum?</a>)"
"auf <span class=\"brand\">keys.openpgp.org</span> veröffentlicht werden. (<a "
"href=\"/about/faq#non-email-uids\" target=\"_blank\">Warum?</a>)"
#: src/gettext_strings.rs:49
msgid ""
"This key contains one revoked identity, which is not published. (<a "
"href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
"This key contains one revoked identity, which is not published. (<a href=\"/"
"about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
msgstr ""
"Dieser Schlüssel enthält eine widerrufene Identität. Diese wird nicht "
"veröffentlicht. (<a href=\"/about/faq#revoked-uids\" "
"target=\"_blank\">Warum?</a>)"
"veröffentlicht. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Warum?"
"</a>)"
#: src/gettext_strings.rs:50
msgid ""
"This key contains {{ count_revoked }} revoked identities, which are not "
"published. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
msgstr ""
"Dieser Schlüssel enthält {{ count_revoked }} widerrufene Identitäten. Diese "
"werden nicht veröffentlicht. (<a href=\"/about/faq#revoked-uids\" "
"target=\"_blank\">Warum?</a>)"
"werden nicht veröffentlicht. (<a href=\"/about/faq#revoked-uids\" target="
"\"_blank\">Warum?</a>)"
#: src/gettext_strings.rs:51
msgid "Your keys have been successfully uploaded:"
msgstr "Schlüssel erfolgreich hochgeladen:"
#: src/gettext_strings.rs:52
msgid ""
"<strong>Note:</strong> To make keys searchable by email address, you must "
"upload them individually."
@@ -352,131 +273,131 @@ msgstr ""
"<strong>Hinweis:</strong> Um Schlüssel für die Email-Adresssuche zu "
"bestätigen, müssen sie jeweils einzeln hochgeladen werden."
#: src/gettext_strings.rs:53
msgid "Verifying your email address…"
msgstr "Email-Adresse wird bestätigt..."
#: src/gettext_strings.rs:54
msgid ""
"If the process doesn't complete after a few seconds, please <input "
"type=\"submit\" class=\"textbutton\" value=\"click here\" />."
"If the process doesn't complete after a few seconds, please <input type="
"\"submit\" class=\"textbutton\" value=\"click here\" />."
msgstr ""
"Wenn der Vorgang nicht in einigen Sekunden erfolgreich ist, bitte<input "
"type=\"submit\" class=\"textbutton\" value=\"hier klicken\"/>."
"Wenn der Vorgang nicht in einigen Sekunden erfolgreich ist, bitte<input type="
"\"submit\" class=\"textbutton\" value=\"hier klicken\"/>."
#: src/gettext_strings.rs:56
msgid "Manage your key on {{domain}}"
msgstr "Schlüssel-Verwaltung auf {{domain}}"
#: src/gettext_strings.rs:58
msgid "Hi,"
msgstr "Hi,"
#: src/gettext_strings.rs:59
msgid ""
"This is an automated message from <a href=\"{{base_uri}}\" style=\"text-"
"decoration:none; color: #333\">{{domain}}</a>."
msgstr ""
"Dies ist eine automatisierte Nachricht von <a href=\"{{base_uri}}\" style"
"=\"text-decoration:none; color: #333\">{{ domain }}</a>."
"Dies ist eine automatisierte Nachricht von <a href=\"{{base_uri}}\" style="
"\"text-decoration:none; color: #333\">{{ domain }}</a>."
#: src/gettext_strings.rs:60
msgid "If you didn't request this message, please ignore it."
msgstr "Falls dies unerwartet ist, bitte die Nachricht ignorieren."
#: src/gettext_strings.rs:61
msgid "OpenPGP key: <tt>{{primary_fp}}</tt>"
msgstr "OpenPGP Schlüssel: <tt>{{primary_fp}}</tt>"
#: src/gettext_strings.rs:62
msgid ""
"To manage and delete listed addresses on this key, please follow the link "
"below:"
msgstr ""
"Du kannst die Identitäten dieses Schlüssels unter folgendem Link verwalten:"
#: src/gettext_strings.rs:63
msgid ""
"You can find more info at <a "
"href=\"{{base_uri}}/about\">{{domain}}/about</a>."
"You can find more info at <a href=\"{{base_uri}}/about\">{{domain}}/about</"
"a>."
msgstr ""
"Weiter Informationen findest du unter <a "
"href=\"{{base_uri}}/about\">{{domain}}/about</a>."
"Weiter Informationen findest du unter <a href=\"{{base_uri}}/about"
"\">{{domain}}/about</a>."
#: src/gettext_strings.rs:64
msgid "distributing OpenPGP keys since 2019"
msgstr "Verzeichnis für OpenPGP-Schlüssel seit 2019"
#: src/gettext_strings.rs:67
msgid "This is an automated message from {{domain}}."
msgstr "Dies ist eine automatische Nachricht von {{domain}}."
#: src/gettext_strings.rs:69
msgid "OpenPGP key: {{primary_fp}}"
msgstr "OpenPGP Schlüssel: {{primary_fp}}"
#: src/gettext_strings.rs:71
msgid "You can find more info at {{base_uri}}/about"
msgstr "Weiter Informationen findest du unter {{base_uri}}/about"
#: src/gettext_strings.rs:74
msgid "Verify {{userid}} for your key on {{domain}}"
msgstr "Bestätige {{userid}} für deinen Schlüssel auf {{domain}}"
#: src/gettext_strings.rs:80
msgid ""
"To let others find this key from your email address \"<a rel=\"nofollow\" "
"href=\"#\" style=\"text-decoration:none; color: #333\">{{userid}}</a>\", "
"please click the link below:"
msgstr ""
"Damit der Schlüssel über die Email-Adresse \"<a rel=\"nofollow\" href=\"#\" "
"style=\"text-decoration:none; color: #333\">{{userid}}</a>\" gefunden werden"
" kann, klicke den folgenden Link:"
"style=\"text-decoration:none; color: #333\">{{userid}}</a>\" gefunden werden "
"kann, klicke den folgenden Link:"
#: src/gettext_strings.rs:88
msgid ""
"To let others find this key from your email address \"{{userid}}\",\n"
"please follow the link below:"
msgstr ""
"Damit der Schlüssel über die Email-Adresse \"{{userid}}\" gefunden werden kann,\n"
"Damit der Schlüssel über die Email-Adresse \"{{userid}}\" gefunden werden "
"kann,\n"
"klicke den folgenden Link:"
#: src/web/manage.rs:103
msgid "No key found for fingerprint {}"
msgstr "Kein Schlüssel gefunden für Fingerprint {}"
msgid "No key found for key id {}"
msgstr "Kein Schlüssel gefunden für Schlüssel-Id {}"
msgid "No key found for email address {}"
msgstr "Kein Schlüssel gefunden für Email-Adresse {}"
msgid "Search by Short Key ID is not supported."
msgstr "Suche nach kurzer Schlüssel-ID wird nicht unterstützt."
msgid "Invalid search query."
msgstr "Ungültige Suchanfrage."
msgctxt "Subject for verification email, {0} = userid, {1} = keyserver domain"
msgid "Verify {0} for your key on {1}"
msgstr "Bestätige {0} für deinen Schlüssel auf {1}"
msgctxt "Subject for manage email, {} = keyserver domain"
msgid "Manage your key on {}"
msgstr "Schlüssel-Verwaltung auf {}"
msgid "This link is invalid or expired"
msgstr "Dieser Link ist ungültig, oder bereits abgelaufen."
#: src/web/manage.rs:129
msgid "Malformed address: {address}"
msgstr "Ungültiges Adress-Format: {address}"
#, fuzzy
msgid "Malformed address: {}"
msgstr "Ungültiges Adress-Format: {}"
#: src/web/manage.rs:136
msgid "No key for address: {address}"
msgstr "Kein Schlüssel gefunden für {address}"
#, fuzzy
msgid "No key for address: {}"
msgstr "Kein Schlüssel gefunden für {}"
#: src/web/manage.rs:152
msgid "A request has already been sent for this address recently."
msgstr "Eine E-Mail für diesen Schlüssel wurde erst kürzlich versandt."
#: src/web/vks.rs:111
msgid "Parsing of key data failed."
msgstr "Fehler bei Verarbeitung des Schlüssel-Materials."
#: src/web/vks.rs:120
msgid "Whoops, please don't upload secret keys!"
msgstr "Ups, bitte keine geheimen Schlüssel hochladen!"
#: src/web/vks.rs:133
msgid "No key uploaded."
msgstr "Es wurde kein Schlüssel hochgeladen."
#: src/web/vks.rs:177
msgid "Error processing uploaded key."
msgstr "Fehler bei Verarbeitung des hochgeladenen Schlüssels."
#: src/web/vks.rs:247
msgid "Upload session expired. Please try again."
msgstr "Zeitlimit beim Hochladen abgelaufen. Bitte versuch es erneut."
#: src/web/vks.rs:284
msgid "Invalid verification link."
msgstr "Ungültiger Bestätigungs-Link."

View File

@@ -12,80 +12,38 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: src/mail.rs:107
#, fuzzy
msgctxt "Subject for verification email"
msgid "Verify {userid} for your key on {domain}"
msgstr "Verify {{userid}} for your key on {{domain}}"
#: src/mail.rs:140
#, fuzzy
msgctxt "Subject for manage email"
msgid "Manage your key on {domain}"
msgstr "Manage your key on {{domain}}"
#: src/i18n_helpers.rs:8
msgid "No key found for fingerprint {fingerprint}"
msgstr ""
#: src/i18n_helpers.rs:10
msgid "No key found for key id {key_id}"
msgstr ""
#: src/i18n_helpers.rs:12
msgid "No key found for email address {email}"
msgstr ""
#: src/i18n_helpers.rs:13
msgid "Search by Short Key ID is not supported."
msgstr ""
#: src/i18n_helpers.rs:14
msgid "Invalid search query."
msgstr ""
#: src/gettext_strings.rs:4
msgid "Error"
msgstr "Error"
#: src/gettext_strings.rs:5
msgid "Looks like something went wrong :("
msgstr "Looks like something went wrong :("
#: src/gettext_strings.rs:6
#, fuzzy
msgid "Error message: {{ internal_error }}"
msgstr "<strong>Error:</strong> {{ internal_error }}"
#: src/gettext_strings.rs:7
msgid "There was an error with your request:"
msgstr ""
#: src/gettext_strings.rs:8
#, fuzzy
msgid "We found an entry for <span class=\"email\">{{ query }}</span>:"
msgstr "We found an entry for <span class=\"email\">{{ query }}</span>:"
#: src/gettext_strings.rs:9
msgid ""
"<strong>Hint:</strong> It's more convenient to use <span class=\"brand"
"\">keys.openpgp.org</span> from your OpenPGP software.<br /> Take a look at "
"our <a href=\"/about/usage\">usage guide</a> for details."
msgstr ""
#: src/gettext_strings.rs:10
msgid "debug info"
msgstr "debug info"
#: src/gettext_strings.rs:11
msgid "Search by Email Address / Key ID / Fingerprint"
msgstr "Search by Email Address / Key ID / Fingerprint"
#: src/gettext_strings.rs:12
msgid "Search"
msgstr "Search"
#: src/gettext_strings.rs:13
msgid ""
"You can also <a href=\"/upload\">upload</a> or <a href=\"/manage\">manage</"
"a> your key."
@@ -93,30 +51,24 @@ msgstr ""
"You can also <a href=\"/upload\">upload</a> or <a href=\"/manage\">manage</"
"a> your key."
#: src/gettext_strings.rs:14
msgid "Find out more <a href=\"/about\">about this service</a>."
msgstr "Find out more <a href=\"/about\">about this service</a>."
#: src/gettext_strings.rs:15
msgid "News:"
msgstr "News:"
#: src/gettext_strings.rs:16
msgid ""
"<a href=\"/about/news#2019-11-12-celebrating-100k\">Celebrating 100.000 "
"verified addresses! 📈</a> (2019-11-12)"
msgstr ""
#: src/gettext_strings.rs:17
#, fuzzy
msgid "v{{ version }} built from"
msgstr "v{{ version }} built from"
#: src/gettext_strings.rs:18
msgid "Powered by <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>"
msgstr "Powered by <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>"
#: src/gettext_strings.rs:19
msgid ""
"Background image retrieved from <a href=\"https://www.toptal.com/designers/"
"subtlepatterns/subtle-grey/\">Subtle Patterns</a> under CC BY-SA 3.0"
@@ -124,24 +76,19 @@ msgstr ""
"Background image retrieved from <a href=\"https://www.toptal.com/designers/"
"subtlepatterns/subtle-grey/\">Subtle Patterns</a> under CC BY-SA 3.0"
#: src/gettext_strings.rs:20
msgid "Maintenance Mode"
msgstr "Maintenance Mode"
#: src/gettext_strings.rs:21
msgid "Manage your key"
msgstr "Manage your key"
#: src/gettext_strings.rs:22
#, fuzzy
msgid "Enter any verified email address for your key"
msgstr "Enter any verified e-mail address of your key"
#: src/gettext_strings.rs:23
msgid "Send link"
msgstr "Send link"
#: src/gettext_strings.rs:24
#, fuzzy
msgid ""
"We will send you an email with a link you can use to remove any of your "
@@ -150,7 +97,6 @@ msgstr ""
"We will send you an e-mail with a link you can use to remove any of your e-"
"mail addresses from search."
#: src/gettext_strings.rs:25
#, fuzzy
msgid ""
"Managing the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
@@ -159,15 +105,12 @@ msgstr ""
"Managing the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
#: src/gettext_strings.rs:26
msgid "Your key is published with the following identity information:"
msgstr "Your key is published with the following identity information:"
#: src/gettext_strings.rs:27
msgid "Delete"
msgstr ""
#: src/gettext_strings.rs:28
msgid ""
"Clicking \"delete\" on any address will remove it from this key. It will no "
"longer appear in a search.<br /> To add another address, <a href=\"/upload"
@@ -177,7 +120,6 @@ msgstr ""
"longer appear in a search.<br /> To add another address, <a href=\"/upload"
"\">upload</a> the key again."
#: src/gettext_strings.rs:29
#, fuzzy
msgid ""
"Your key is published as only non-identity information. (<a href=\"/about\" "
@@ -186,11 +128,9 @@ msgstr ""
"Your key is published as only non-identity information. (<a href=\"/about\" "
"target=\"_blank\">what does this mean?</a>)"
#: src/gettext_strings.rs:30
msgid "To add an address, <a href=\"/upload\">upload</a> the key again."
msgstr "To add an address, <a href=\"/upload\">upload</a> the key again."
#: src/gettext_strings.rs:31
#, fuzzy
msgid ""
"We have sent an email with further instructions to <span class=\"email"
@@ -199,12 +139,10 @@ msgstr ""
"We have sent an email with further instructions to <span class=\"email"
"\">{{ address }}</span>"
#: src/gettext_strings.rs:32
#, fuzzy
msgid "This address has already been verified."
msgstr "This address was already verified."
#: src/gettext_strings.rs:33
#, fuzzy
msgid ""
"Your key <span class=\"fingerprint\">{{ key_fpr }}</span> is now published "
@@ -215,16 +153,13 @@ msgstr ""
"for the identity <a href=\"{{userid_link}}\" target=\"_blank\"><span class="
"\"email\">{{ userid }}</span></a>."
#: src/gettext_strings.rs:34
#, fuzzy
msgid "Upload your key"
msgstr "Manage your key"
#: src/gettext_strings.rs:35
msgid "Upload"
msgstr "Upload"
#: src/gettext_strings.rs:36
#, fuzzy
msgid ""
"Need more info? Check our <a target=\"_blank\" href=\"/about\">intro</a> and "
@@ -233,7 +168,6 @@ msgstr ""
"Need more info? Check our <a target=\"_blank\" href=\"/about\">intro</a> and "
"<a target=\"_blank\" href=\"/about/usage\">usage guide</a>!"
#: src/gettext_strings.rs:37
#, fuzzy
msgid ""
"You uploaded the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
@@ -242,11 +176,9 @@ msgstr ""
"You uploaded the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
#: src/gettext_strings.rs:38
msgid "This key is revoked."
msgstr "This key is revoked."
#: src/gettext_strings.rs:39
#, fuzzy
msgid ""
"It is published without identity information and can't be made available for "
@@ -256,7 +188,6 @@ msgstr ""
"It is published without identity information and can't be made available for "
"search by e-mail address"
#: src/gettext_strings.rs:40
msgid ""
"This key is now published with the following identity information (<a href="
"\"/about\" target=\"_blank\">what does this mean?</a>):"
@@ -264,11 +195,9 @@ msgstr ""
"This key is now published with the following identity information (<a href="
"\"/about\" target=\"_blank\">what does this mean?</a>):"
#: src/gettext_strings.rs:41
msgid "Published"
msgstr "Published"
#: src/gettext_strings.rs:42
#, fuzzy
msgid ""
"This key is now published with only non-identity information. (<a href=\"/"
@@ -277,7 +206,6 @@ msgstr ""
"This key is now published with only non-identity information (<a href=\"/"
"about\" target=\"_blank\">what does this mean?</a>)"
#: src/gettext_strings.rs:43
#, fuzzy
msgid ""
"To make the key available for search by email address, you can verify it "
@@ -286,22 +214,18 @@ msgstr ""
"To make the key available for search by e-mail address, you can verify it "
"belongs to you:"
#: src/gettext_strings.rs:44
msgid "Verification Pending"
msgstr "Verification Pending"
#: src/gettext_strings.rs:45
msgid ""
"<strong>Note:</strong> Some providers delay emails for up to 15 minutes to "
"prevent spam. Please be patient."
msgstr ""
#: src/gettext_strings.rs:46
#, fuzzy
msgid "Send Verification Email"
msgstr "Send Verification Mail"
#: src/gettext_strings.rs:47
#, fuzzy
msgid ""
"This key contains one identity that could not be parsed as an email address."
@@ -314,7 +238,6 @@ msgstr ""
"openpgp.org</span>. (<a href=\"/about/faq#non-email-uids\" target=\"_blank"
"\">why?</a>)"
#: src/gettext_strings.rs:48
#, fuzzy
msgid ""
"This key contains {{ count_unparsed }} identities that could not be parsed "
@@ -327,14 +250,12 @@ msgstr ""
"class=\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-email-"
"uids\" target=\"_blank\">why?</a>)"
#: src/gettext_strings.rs:49
#, fuzzy
msgid ""
"This key contains one revoked identity, which is not published. (<a href=\"/"
"about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
msgstr "This key contains one revoked identity, which is not published."
#: src/gettext_strings.rs:50
#, fuzzy
msgid ""
"This key contains {{ count_revoked }} revoked identities, which are not "
@@ -343,11 +264,9 @@ msgstr ""
"This key contains {{ count_revoked }} revoked identities, which are not "
"published."
#: src/gettext_strings.rs:51
msgid "Your keys have been successfully uploaded:"
msgstr "Your keys have been successfully uploaded:"
#: src/gettext_strings.rs:52
#, fuzzy
msgid ""
"<strong>Note:</strong> To make keys searchable by email address, you must "
@@ -356,11 +275,9 @@ msgstr ""
"<strong>Note:</strong> To make keys searchable by address, you must upload "
"them individually."
#: src/gettext_strings.rs:53
msgid "Verifying your email address…"
msgstr "Verifying your email address…"
#: src/gettext_strings.rs:54
#, fuzzy
msgid ""
"If the process doesn't complete after a few seconds, please <input type="
@@ -369,15 +286,12 @@ msgstr ""
"If the process doesn't complete after a few seconds, <input type=\"submit\" "
"class=\"textbutton\" value=\"click here\" />."
#: src/gettext_strings.rs:56
msgid "Manage your key on {{domain}}"
msgstr "Manage your key on {{domain}}"
#: src/gettext_strings.rs:58
msgid "Hi,"
msgstr "Hi,"
#: src/gettext_strings.rs:59
#, fuzzy
msgid ""
"This is an automated message from <a href=\"{{base_uri}}\" style=\"text-"
@@ -387,15 +301,12 @@ msgstr ""
"\"{{ base_uri }}\" style=\"text-decoration:none; color: "
"#333\"><tt>{{ domain }}</tt></a>."
#: src/gettext_strings.rs:60
msgid "If you didn't request this message, please ignore it."
msgstr "If you didn't request this message, please ignore it."
#: src/gettext_strings.rs:61
msgid "OpenPGP key: <tt>{{primary_fp}}</tt>"
msgstr "OpenPGP key: <tt>{{primary_fp}}</tt>"
#: src/gettext_strings.rs:62
msgid ""
"To manage and delete listed addresses on this key, please follow the link "
"below:"
@@ -403,7 +314,6 @@ msgstr ""
"To manage and delete listed addresses on this key, please follow the link "
"below:"
#: src/gettext_strings.rs:63
msgid ""
"You can find more info at <a href=\"{{base_uri}}/about\">{{domain}}/about</"
"a>."
@@ -411,28 +321,22 @@ msgstr ""
"You can find more info at <a href=\"{{base_uri}}/about\">{{domain}}/about</"
"a>."
#: src/gettext_strings.rs:64
msgid "distributing OpenPGP keys since 2019"
msgstr ""
#: src/gettext_strings.rs:67
#, fuzzy
msgid "This is an automated message from {{domain}}."
msgstr "this is an automated message from {{domain}}. If you didn't"
#: src/gettext_strings.rs:69
msgid "OpenPGP key: {{primary_fp}}"
msgstr "OpenPGP key: {{primary_fp}}"
#: src/gettext_strings.rs:71
msgid "You can find more info at {{base_uri}}/about"
msgstr "You can find more info at {{base_uri}}/about"
#: src/gettext_strings.rs:74
msgid "Verify {{userid}} for your key on {{domain}}"
msgstr "Verify {{userid}} for your key on {{domain}}"
#: src/gettext_strings.rs:80
msgid ""
"To let others find this key from your email address \"<a rel=\"nofollow\" "
"href=\"#\" style=\"text-decoration:none; color: #333\">{{userid}}</a>\", "
@@ -442,55 +346,78 @@ msgstr ""
"href=\"#\" style=\"text-decoration:none; color: #333\">{{userid}}</a>\", "
"please click the link below:"
#: src/gettext_strings.rs:88
#, fuzzy
msgid ""
"To let others find this key from your email address \"{{userid}}\",\n"
"please follow the link below:"
msgstr "To let others find this key from your email address \"{{userid}}\","
#: src/web/manage.rs:103
msgid "No key found for fingerprint {}"
msgstr "No key found for fingerprint {}"
msgid "No key found for key id {}"
msgstr "No key found for key id {}"
msgid "No key found for email address {}"
msgstr "No key found for email address {}"
msgid "Search by Short Key ID is not supported."
msgstr ""
msgid "Invalid search query."
msgstr ""
msgctxt "Subject for verification email, {0} = userid, {1} = keyserver domain"
msgid "Verify {0} for your key on {1}"
msgstr "Verify {0} for your key on {1}"
msgctxt "Subject for manage email, {} = keyserver domain"
msgid "Manage your key on {}"
msgstr "Manage your key on {}"
msgid "This link is invalid or expired"
msgstr ""
#: src/web/manage.rs:129
msgid "Malformed address: {address}"
msgid "Malformed address: {}"
msgstr ""
#: src/web/manage.rs:136
msgid "No key for address: {address}"
msgstr ""
#, fuzzy
msgid "No key for address: {}"
msgstr "Verifying your email address…"
#: src/web/manage.rs:152
msgid "A request has already been sent for this address recently."
msgstr ""
#: src/web/vks.rs:111
msgid "Parsing of key data failed."
msgstr ""
#: src/web/vks.rs:120
msgid "Whoops, please don't upload secret keys!"
msgstr ""
#: src/web/vks.rs:133
#, fuzzy
msgid "No key uploaded."
msgstr "Your key upload on {}"
#: src/web/vks.rs:177
msgid "Error processing uploaded key."
msgstr ""
#: src/web/vks.rs:247
msgid "Upload session expired. Please try again."
msgstr ""
#: src/web/vks.rs:284
#, fuzzy
msgid "Invalid verification link."
msgstr "Send Verification Mail"
#, fuzzy
#~ msgctxt "Subject for verification email"
#~ msgid "Verify {userid} for your key on {domain}"
#~ msgstr "Verify {{userid}} for your key on {{domain}}"
#, fuzzy
#~ msgctxt "Subject for manage email"
#~ msgid "Manage your key on {domain}"
#~ msgstr "Manage your key on {{domain}}"
#, fuzzy
#~ msgctxt "Subject for welcome email"
#~ msgid "Your key upload on {domain}"
@@ -618,12 +545,6 @@ msgstr "Send Verification Mail"
#~ msgid "what does this mean?"
#~ msgstr "what does this mean?"
#~ msgid "Verify {} for your key on {}"
#~ msgstr "Verify {} for your key on {}"
#~ msgid "Manage your key on {}"
#~ msgstr "Manage your key on {}"
#~ msgid "(<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
#~ msgstr "(<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"

402
po/hagrid/es.po Normal file
View File

@@ -0,0 +1,402 @@
#
# Translators:
# Luis Figueroa, 2021
#
msgid ""
msgstr ""
"Project-Id-Version: hagrid\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2019-09-27 18:05+0000\n"
"Last-Translator: Luis Figueroa, 2021\n"
"Language-Team: Spanish (https://www.transifex.com/otf/teams/102430/es/)\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "Error"
msgstr "Error"
msgid "Looks like something went wrong :("
msgstr "Parece que algo salió mal :("
msgid "Error message: {{ internal_error }}"
msgstr "Mensaje de error: {{ internal_error }}"
msgid "There was an error with your request:"
msgstr "Hubo un error con tu solicitud:"
msgid "We found an entry for <span class=\"email\">{{ query }}</span>:"
msgstr "Encontramos un registro para <span class=\"email\">{{ query }}</span>:"
msgid ""
"<strong>Hint:</strong> It's more convenient to use <span class=\"brand"
"\">keys.openpgp.org</span> from your OpenPGP software.<br /> Take a look at "
"our <a href=\"/about/usage\">usage guide</a> for details."
msgstr ""
"<strong>Consejo:</strong> Es más conveniente usar <span class=\"brand\">keys."
"openpgp.org</span> desde tu software con OpenPGP.<br /> Echa un vistazo a "
"nuestra <a href=\"/about/usage\">guía de uso</a> para más detalles."
msgid "debug info"
msgstr "información de depuración"
msgid "Search by Email Address / Key ID / Fingerprint"
msgstr "Busca por Dirección de Correo / ID de Clave / Huella Digital"
msgid "Search"
msgstr "Buscar"
msgid ""
"You can also <a href=\"/upload\">upload</a> or <a href=\"/manage\">manage</"
"a> your key."
msgstr ""
"También puedes <a href=\"/upload\">subir</a> o <a href=\"/manage"
"\">administrar</a> tu clave."
msgid "Find out more <a href=\"/about\">about this service</a>."
msgstr "Encuentra más <a href=\"/about\">acerca de este servicio</a>."
msgid "News:"
msgstr "Noticias:"
msgid ""
"<a href=\"/about/news#2019-11-12-celebrating-100k\">Celebrating 100.000 "
"verified addresses! 📈</a> (2019-11-12)"
msgstr ""
"<a href=\"/about/news#2019-11-12-celebrating-100k\">¡Celebrando 100.000 "
"direcciones verificadas! 📈</a> (2019-11-12)"
msgid "v{{ version }} built from"
msgstr "v{{ version }} hecho desde"
msgid "Powered by <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>"
msgstr "impulsado por <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>"
msgid ""
"Background image retrieved from <a href=\"https://www.toptal.com/designers/"
"subtlepatterns/subtle-grey/\">Subtle Patterns</a> under CC BY-SA 3.0"
msgstr ""
"Imagen de fondo tomada de <a href=\"https://www.toptal.com/designers/"
"subtlepatterns/subtle-grey/\">Subtle Patterns</a> bajo licencia CC BY-SA 3.0"
msgid "Maintenance Mode"
msgstr "Modo Mantenimiento"
msgid "Manage your key"
msgstr "Administra tu clave"
msgid "Enter any verified email address for your key"
msgstr "Ingresa cualquier dirección de correo verificada para tu clave"
msgid "Send link"
msgstr "Enviar enlace"
msgid ""
"We will send you an email with a link you can use to remove any of your "
"email addresses from search."
msgstr ""
"Te enviaremos un correo con un enlace que puedes usar para eliminar "
"cualquiera de tus direcciones de correo de la búsqueda."
msgid ""
"Managing the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
msgstr ""
"Administrando la clave <span class=\"fingerprint\"><a href="
"\"{{ key_link }}\" target=\"_blank\">{{ key_fpr }}</a></span>."
msgid "Your key is published with the following identity information:"
msgstr "Tu clave está publicada con la siguiente información de identidad:"
msgid "Delete"
msgstr "Eliminar"
msgid ""
"Clicking \"delete\" on any address will remove it from this key. It will no "
"longer appear in a search.<br /> To add another address, <a href=\"/upload"
"\">upload</a> the key again."
msgstr ""
"Haciendo clic en \"Eliminar\" en cualquier dirección la removerá de esta "
"llave. No aparecerá más en una búsqueda.<br />Para añadir otra dirección, <a "
"href=\"/upload\">sube</a> la clave de nuevo."
msgid ""
"Your key is published as only non-identity information. (<a href=\"/about\" "
"target=\"_blank\">What does this mean?</a>)"
msgstr ""
"Tu clave está publicada sin información de identidad. (<a href=\"/about\" "
"target=\"_blank\">¿Qué significa esto?</a>)"
msgid "To add an address, <a href=\"/upload\">upload</a> the key again."
msgstr ""
"Para añadir una dirección de correo, <a href=\"/upload\">sube</a> la clave "
"de nuevo."
msgid ""
"We have sent an email with further instructions to <span class=\"email"
"\">{{ address }}</span>."
msgstr ""
"Te hemos enviado un correo con más instrucciones a <span class=\"email"
"\">{{ address }}</span>."
msgid "This address has already been verified."
msgstr "Esta dirección ya ha sido verificada."
msgid ""
"Your key <span class=\"fingerprint\">{{ key_fpr }}</span> is now published "
"for the identity <a href=\"{{userid_link}}\" target=\"_blank\"><span class="
"\"email\">{{ userid }}</span></a>."
msgstr ""
"Tu clave <span class=\"fingerprint\">{{ key_fpr }}</span> ahora está "
"publicada con la identidad <a href=\"{{userid_link}}\" target=\"_blank"
"\"><span class=\"email\">{{ userid }}</span></a>."
msgid "Upload your key"
msgstr "Sube tu clave"
msgid "Upload"
msgstr "Subir"
msgid ""
"Need more info? Check our <a target=\"_blank\" href=\"/about\">intro</a> and "
"<a target=\"_blank\" href=\"/about/usage\">usage guide</a>."
msgstr ""
"¿Necesitas más información? Mira nuestra <a target=\"_blank\" href=\"/about"
"\">introducción</a> y <a target=\"_blank\" href=\"/about/usage\">guía de "
"uso</a>."
msgid ""
"You uploaded the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
msgstr ""
"Subiste la clave <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
msgid "This key is revoked."
msgstr "Esta clave está revocada."
msgid ""
"It is published without identity information and can't be made available for "
"search by email address (<a href=\"/about\" target=\"_blank\">what does this "
"mean?</a>)."
msgstr ""
"Está publicada sin información de identidad y no puede hacerse disponible "
"para buscar por dirección de correo electrónico. (<a href=\"/about\" target="
"\"_blank\">¿Qué significa esto?</a>)."
msgid ""
"This key is now published with the following identity information (<a href="
"\"/about\" target=\"_blank\">what does this mean?</a>):"
msgstr ""
"Esta clave ahora está publicada con la siguiente información de identidad "
"(<a href=\"/about\" target=\"_blank\">¿Qué significa esto?</a>):"
msgid "Published"
msgstr "Publicada"
msgid ""
"This key is now published with only non-identity information. (<a href=\"/"
"about\" target=\"_blank\">What does this mean?</a>)"
msgstr ""
"Esta clave ahora está publicada sin información de identidad. (<a href=\"/"
"about\" target=\"_blank\">¿Qué significa esto?</a>)"
msgid ""
"To make the key available for search by email address, you can verify it "
"belongs to you:"
msgstr ""
"Para hacer la clave disponible en la búsqueda con dirección de correo, "
"puedes verificar que te pertenece:"
msgid "Verification Pending"
msgstr "Verificación Pendiente"
msgid ""
"<strong>Note:</strong> Some providers delay emails for up to 15 minutes to "
"prevent spam. Please be patient."
msgstr ""
"<strong>Nota:</strong> Algunos proveedores retrasan los correos hasta por 15 "
"minutos para prevenir correo no deseado. Por favor sé paciente."
msgid "Send Verification Email"
msgstr "Enviar Correo de Verificación"
msgid ""
"This key contains one identity that could not be parsed as an email address."
"<br /> This identity can't be published on <span class=\"brand\">keys."
"openpgp.org</span>. (<a href=\"/about/faq#non-email-uids\" target=\"_blank"
"\">Why?</a>)"
msgstr ""
"Esta clave contiene una identidad que no pudo ser interpretada como una "
"dirección de correo.<br />Esta identidad no puede ser publicada en <span "
"class=\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-email-uids"
"\" target=\"_blank\">¿Por qué?</a>)"
msgid ""
"This key contains {{ count_unparsed }} identities that could not be parsed "
"as an email address.<br /> These identities can't be published on <span "
"class=\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-email-"
"uids\" target=\"_blank\">Why?</a>)"
msgstr ""
"Esta clave contiene {{ count_unparsed }} identidades que no pudieron ser "
"interpretadas como una dirección de correo.<br />Estas identidades no pueden "
"ser publicadas en <span class=\"brand\">keys.openpgp.org</span>. (<a href=\"/"
"about/faq#non-email-uids\" target=\"_blank\">¿Por qué?</a>)"
msgid ""
"This key contains one revoked identity, which is not published. (<a href=\"/"
"about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
msgstr ""
"Esta clave contiene una identidad revocada, la cual no está publicada. (<a "
"href=\"/about/faq#revoked-uids\" target=\"_blank\">¿Por qué?)"
msgid ""
"This key contains {{ count_revoked }} revoked identities, which are not "
"published. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
msgstr ""
"Esta clave contiene {{ count_revoked }} identidades revocadas, las cual no "
"están publicadas. (<a href=\"/about/faq#revoked-uids\" target=\"_blank"
"\">¿Por qué?)"
msgid "Your keys have been successfully uploaded:"
msgstr "Tus claves han sido subidas exitosamente:"
msgid ""
"<strong>Note:</strong> To make keys searchable by email address, you must "
"upload them individually."
msgstr ""
"<strong>Nota:</strong> Para hacer que las claves se puedan buscar por "
"dirección de correo, debes subirlas individualmente."
msgid "Verifying your email address…"
msgstr "Verificando tu dirección de correo..."
msgid ""
"If the process doesn't complete after a few seconds, please <input type="
"\"submit\" class=\"textbutton\" value=\"click here\" />."
msgstr ""
"Si el proceso no se completa luego de unos segundos, por favor <input type="
"\"submit\" class=\"textbutton\" value=\"click here\" />."
msgid "Manage your key on {{domain}}"
msgstr "Administra tu clave en {{domain}}"
msgid "Hi,"
msgstr "Hola,"
msgid ""
"This is an automated message from <a href=\"{{base_uri}}\" style=\"text-"
"decoration:none; color: #333\">{{domain}}</a>."
msgstr ""
"Este es un mensaje automatizado de <a href=\"{{base_uri}}\" style=\"text-"
"decoration:none; color: #333\">{{domain}}</a>."
msgid "If you didn't request this message, please ignore it."
msgstr "Si tú no solicitaste este mensaje, por favor ignóralo."
msgid "OpenPGP key: <tt>{{primary_fp}}</tt>"
msgstr "Clave OpenPGP: <tt>{{primary_fp}}</tt>"
msgid ""
"To manage and delete listed addresses on this key, please follow the link "
"below:"
msgstr ""
"Para administrar y eliminar las direcciones listadas en esta clave, por "
"favor ve al siguiente enlace:"
msgid ""
"You can find more info at <a href=\"{{base_uri}}/about\">{{domain}}/about</"
"a>."
msgstr ""
"Puedes encontrar más información en <a href=\"{{base_uri}}/about"
"\">{{domain}}/about</a>."
msgid "distributing OpenPGP keys since 2019"
msgstr "distribuyendo claves OpenPGP desde 2019"
msgid "This is an automated message from {{domain}}."
msgstr "Este es un mensaje automatizado de {{domain}}."
msgid "OpenPGP key: {{primary_fp}}"
msgstr "Clave OpenPGP: {{primary_fp}}"
msgid "You can find more info at {{base_uri}}/about"
msgstr "Puedes encontrar más información en {{base_uri}}/about"
msgid "Verify {{userid}} for your key on {{domain}}"
msgstr "Verifica {{userid}} para tu clave en {{domain}}"
msgid ""
"To let others find this key from your email address \"<a rel=\"nofollow\" "
"href=\"#\" style=\"text-decoration:none; color: #333\">{{userid}}</a>\", "
"please click the link below:"
msgstr ""
"Para permitir que otros encuentren esta clave mediante tu dirección de "
"correo \"<a rel=\"nofollow\" href=\"#\" style=\"text-decoration:none; color: "
"#333\">{{userid}}</a>\", por favor haz clic en el siguiente enlace:"
msgid ""
"To let others find this key from your email address \"{{userid}}\",\n"
"please follow the link below:"
msgstr ""
"Para permitir que otros encuentren esta clave mediante tu dirección de "
"correo \"{{userid}}\", \n"
"por favor haz clic en el siguiente enlace:"
msgid "No key found for fingerprint {}"
msgstr "No se encontraron claves para la huella digital {}"
msgid "No key found for key id {}"
msgstr "No se encontraron claves para el ID de clave {}"
msgid "No key found for email address {}"
msgstr "No se encontraron claves para la dirección de correo electrónico {}"
msgid "Search by Short Key ID is not supported."
msgstr "Buscar por ID de Short Key no está soportado."
msgid "Invalid search query."
msgstr "Búsqueda inválida."
msgctxt "Subject for verification email, {0} = userid, {1} = keyserver domain"
msgid "Verify {0} for your key on {1}"
msgstr "Verifica {0} para tu clave en {1}"
msgctxt "Subject for manage email, {} = keyserver domain"
msgid "Manage your key on {}"
msgstr "Administra tu clave en {}"
msgid "This link is invalid or expired"
msgstr "Este enlace es inválido o caducado"
#, fuzzy
msgid "Malformed address: {}"
msgstr "Dirección malformada: {}"
#, fuzzy
msgid "No key for address: {}"
msgstr "No hay claves para la dirección: {}"
msgid "A request has already been sent for this address recently."
msgstr "Una solicitud para esta dirección ya se ha enviado recientemente."
msgid "Parsing of key data failed."
msgstr "Interpretación de datos de la clave fallida."
msgid "Whoops, please don't upload secret keys!"
msgstr "Ups, ¡por favor no subas claves secretas!"
msgid "No key uploaded."
msgstr "No se subió una clave."
msgid "Error processing uploaded key."
msgstr "Error procesando la clave subida."
msgid "Upload session expired. Please try again."
msgstr "Sesión de subida caducada. Por favor intente de nuevo."
msgid "Invalid verification link."
msgstr "Enlace de verificación inválido."

View File

@@ -17,58 +17,22 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: src/mail.rs:107
msgctxt "Subject for verification email"
msgid "Verify {userid} for your key on {domain}"
msgstr "Confirmer {userid} pour votre clé sur {domain}"
#: src/mail.rs:140
msgctxt "Subject for manage email"
msgid "Manage your key on {domain}"
msgstr "Gérer votre clé sur {domain}"
#: src/i18n_helpers.rs:8
msgid "No key found for fingerprint {fingerprint}"
msgstr "Aucune clé na été trouvée pour lempreinte {fingerprint}"
#: src/i18n_helpers.rs:10
msgid "No key found for key id {key_id}"
msgstr "Aucune clé na été trouvée pour lID de clé {key_id}"
#: src/i18n_helpers.rs:12
msgid "No key found for email address {email}"
msgstr "Aucune clé na été trouvée pour ladresse courriel {email}"
#: src/i18n_helpers.rs:13
msgid "Search by Short Key ID is not supported."
msgstr "La recherche par ID de clé courte nest pas prise en charge."
#: src/i18n_helpers.rs:14
msgid "Invalid search query."
msgstr "La requête dinterrogation est invalide."
#: src/gettext_strings.rs:4
msgid "Error"
msgstr "Erreur"
#: src/gettext_strings.rs:5
msgid "Looks like something went wrong :("
msgstr "Il semble quun problème est survenu :("
#: src/gettext_strings.rs:6
msgid "Error message: {{ internal_error }}"
msgstr "Message derreur : {{ internal_error }}"
#: src/gettext_strings.rs:7
msgid "There was an error with your request:"
msgstr "Votre demande a généré une erreur :"
#: src/gettext_strings.rs:8
msgid "We found an entry for <span class=\"email\">{{ query }}</span>:"
msgstr ""
"Nous avons trouvé une entrée pour <span class=\"email\">{{ query }}</span> :"
#: src/gettext_strings.rs:9
msgid ""
"<strong>Hint:</strong> It's more convenient to use <span class=\"brand"
"\">keys.openpgp.org</span> from your OpenPGP software.<br /> Take a look at "
@@ -79,19 +43,15 @@ msgstr ""
"Vous trouverez plus de précisions dans notre <a href=\"/about/usage\">guide "
"dutilisation</a>."
#: src/gettext_strings.rs:10
msgid "debug info"
msgstr "renseignements de débogage"
#: src/gettext_strings.rs:11
msgid "Search by Email Address / Key ID / Fingerprint"
msgstr "Cherchez par adresse courriel / ID de clé / empreinte"
#: src/gettext_strings.rs:12
msgid "Search"
msgstr "Chercher"
#: src/gettext_strings.rs:13
msgid ""
"You can also <a href=\"/upload\">upload</a> or <a href=\"/manage\">manage</"
"a> your key."
@@ -99,15 +59,12 @@ msgstr ""
"Vous pouvez aussi <a href=\"/upload\">téléverser</a> ou <a href=\"/manage"
"\">gérer</a> votre clé."
#: src/gettext_strings.rs:14
msgid "Find out more <a href=\"/about\">about this service</a>."
msgstr "En apprendre davantage <a href=\"/about\">sur ce service</a>."
#: src/gettext_strings.rs:15
msgid "News:"
msgstr "Nouvelles :"
#: src/gettext_strings.rs:16
msgid ""
"<a href=\"/about/news#2019-11-12-celebrating-100k\">Celebrating 100.000 "
"verified addresses! 📈</a> (2019-11-12)"
@@ -115,15 +72,12 @@ msgstr ""
"<a href=\"/about/news#2019-11-12-celebrating-100k\">Nous célébrons 100"
"000 adresses confirmées! 📈</a> (12-11-2019)"
#: src/gettext_strings.rs:17
msgid "v{{ version }} built from"
msgstr "v{{ version }} compilée à partir de"
#: src/gettext_strings.rs:18
msgid "Powered by <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>"
msgstr "Propulsé par <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>"
#: src/gettext_strings.rs:19
msgid ""
"Background image retrieved from <a href=\"https://www.toptal.com/designers/"
"subtlepatterns/subtle-grey/\">Subtle Patterns</a> under CC BY-SA 3.0"
@@ -132,23 +86,18 @@ msgstr ""
"designers/subtlepatterns/subtle-grey/\">Subtle Patterns</a> sous licence CC "
"BY-SA 3.0"
#: src/gettext_strings.rs:20
msgid "Maintenance Mode"
msgstr "Mode de maintenance"
#: src/gettext_strings.rs:21
msgid "Manage your key"
msgstr "Gérer votre clé"
#: src/gettext_strings.rs:22
msgid "Enter any verified email address for your key"
msgstr "Saisissez une adresse courriel confirmée pour votre clé"
#: src/gettext_strings.rs:23
msgid "Send link"
msgstr "Envoyer le lien"
#: src/gettext_strings.rs:24
msgid ""
"We will send you an email with a link you can use to remove any of your "
"email addresses from search."
@@ -156,7 +105,6 @@ msgstr ""
"Nous vous enverrons un courriel avec un lien que vous pourrez utiliser pour "
"supprimer de la recherche lune de vos adresses courriel."
#: src/gettext_strings.rs:25
msgid ""
"Managing the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
@@ -164,15 +112,12 @@ msgstr ""
"Gestion de la clé <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
#: src/gettext_strings.rs:26
msgid "Your key is published with the following identity information:"
msgstr "Votre clé est publiée avec les renseignements didentité suivants :"
#: src/gettext_strings.rs:27
msgid "Delete"
msgstr "Supprimer"
#: src/gettext_strings.rs:28
msgid ""
"Clicking \"delete\" on any address will remove it from this key. It will no "
"longer appear in a search.<br /> To add another address, <a href=\"/upload"
@@ -182,7 +127,6 @@ msgstr ""
"Elle napparaîtra plus lors dune recherche.<br /> Pour ajouter une autre "
"adresse, <a href=\"/upload\">téléversez</a> la clé de nouveau."
#: src/gettext_strings.rs:29
msgid ""
"Your key is published as only non-identity information. (<a href=\"/about\" "
"target=\"_blank\">What does this mean?</a>)"
@@ -191,13 +135,11 @@ msgstr ""
"vous identifier. (<a href=\"/about\" target=\"_blank\">Quest-ce que cela "
"signifie?</a>)"
#: src/gettext_strings.rs:30
msgid "To add an address, <a href=\"/upload\">upload</a> the key again."
msgstr ""
"Pour ajouter une adresse, <a href=\"/upload\">téléversez</a> la clé de "
"nouveau."
#: src/gettext_strings.rs:31
msgid ""
"We have sent an email with further instructions to <span class=\"email"
"\">{{ address }}</span>."
@@ -205,11 +147,9 @@ msgstr ""
"Un courriel avec de plus amples instructions a été envoyé à <span class="
"\"email\">{{ address }}</span>."
#: src/gettext_strings.rs:32
msgid "This address has already been verified."
msgstr "Cette adresse a déjà été confirmée."
#: src/gettext_strings.rs:33
msgid ""
"Your key <span class=\"fingerprint\">{{ key_fpr }}</span> is now published "
"for the identity <a href=\"{{userid_link}}\" target=\"_blank\"><span class="
@@ -219,15 +159,12 @@ msgstr ""
"publiée pour lidentité <a href=\"{{userid_link}}\" target=\"_blank\"><span "
"class=\"email\">{{ userid }}</span></a>. "
#: src/gettext_strings.rs:34
msgid "Upload your key"
msgstr "Téléverser votre clé"
#: src/gettext_strings.rs:35
msgid "Upload"
msgstr "Téléverser"
#: src/gettext_strings.rs:36
msgid ""
"Need more info? Check our <a target=\"_blank\" href=\"/about\">intro</a> and "
"<a target=\"_blank\" href=\"/about/usage\">usage guide</a>."
@@ -236,7 +173,6 @@ msgstr ""
"about\">présentation</a> et notre <a target=\"_blank\" href=\"/about/usage"
"\">guide dutilisation</a>."
#: src/gettext_strings.rs:37
msgid ""
"You uploaded the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
@@ -244,11 +180,9 @@ msgstr ""
"Vous avez téléversé la clé <span class=\"fingerprint\"><a href="
"\"{{ key_link }}\" target=\"_blank\">{{ key_fpr }}</a></span>."
#: src/gettext_strings.rs:38
msgid "This key is revoked."
msgstr "Cette clé est révoquée."
#: src/gettext_strings.rs:39
msgid ""
"It is published without identity information and can't be made available for "
"search by email address (<a href=\"/about\" target=\"_blank\">what does this "
@@ -258,7 +192,6 @@ msgstr ""
"courriel ne la trouvera pas (<a href=\"/about\" target=\"_blank\">quest-ce "
"que cela signifie?</a>)."
#: src/gettext_strings.rs:40
msgid ""
"This key is now published with the following identity information (<a href="
"\"/about\" target=\"_blank\">what does this mean?</a>):"
@@ -266,11 +199,9 @@ msgstr ""
"Cette clé est maintenant publiée avec les renseignements didentité suivants "
"(<a href=\"/about\" target=\"_blank\">quest-ce que cela signifie?</a>) :"
#: src/gettext_strings.rs:41
msgid "Published"
msgstr "Publiée"
#: src/gettext_strings.rs:42
msgid ""
"This key is now published with only non-identity information. (<a href=\"/"
"about\" target=\"_blank\">What does this mean?</a>)"
@@ -279,7 +210,6 @@ msgstr ""
"permettent pas de vous identifier. (<a href=\"/about\" target=\"_blank"
"\">Quest-ce que cela signifie?</a>)"
#: src/gettext_strings.rs:43
msgid ""
"To make the key available for search by email address, you can verify it "
"belongs to you:"
@@ -287,11 +217,9 @@ msgstr ""
"Afin quune recherche par adresse courriel trouve cette clé, vous pouvez "
"confirmer quelle vous appartient :"
#: src/gettext_strings.rs:44
msgid "Verification Pending"
msgstr "La confirmation est en attente"
#: src/gettext_strings.rs:45
msgid ""
"<strong>Note:</strong> Some providers delay emails for up to 15 minutes to "
"prevent spam. Please be patient."
@@ -300,11 +228,9 @@ msgstr ""
"jusquà 15 minutes afin de prévenir les courriels indésirables (pourriels). "
"Veuillez faire preuve de patience."
#: src/gettext_strings.rs:46
msgid "Send Verification Email"
msgstr "Envoyer un courriel de confirmation"
#: src/gettext_strings.rs:47
msgid ""
"This key contains one identity that could not be parsed as an email address."
"<br /> This identity can't be published on <span class=\"brand\">keys."
@@ -316,7 +242,6 @@ msgstr ""
"class=\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-email-uids"
"\" target=\"_blank\">Pourquoi?</a>) "
#: src/gettext_strings.rs:48
msgid ""
"This key contains {{ count_unparsed }} identities that could not be parsed "
"as an email address.<br /> These identities can't be published on <span "
@@ -328,7 +253,6 @@ msgstr ""
"être publiées sur <span class=\"brand\">keys.openpgp.org</span>. (<a href=\"/"
"about/faq#non-email-uids\" target=\"_blank\">Pourquoi?</a>) "
#: src/gettext_strings.rs:49
msgid ""
"This key contains one revoked identity, which is not published. (<a href=\"/"
"about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
@@ -336,7 +260,6 @@ msgstr ""
"Cette clé comprend une identité révoquée qui nest pas publiée. (<a href=\"/"
"about/faq#revoked-uids\" target=\"_blank\">Pourquoi?</a>)"
#: src/gettext_strings.rs:50
msgid ""
"This key contains {{ count_revoked }} revoked identities, which are not "
"published. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
@@ -345,11 +268,9 @@ msgstr ""
"publiées. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Pourquoi?</"
"a>)"
#: src/gettext_strings.rs:51
msgid "Your keys have been successfully uploaded:"
msgstr "Vos clés ont été téléversées avec succès :"
#: src/gettext_strings.rs:52
msgid ""
"<strong>Note:</strong> To make keys searchable by email address, you must "
"upload them individually."
@@ -357,11 +278,9 @@ msgstr ""
"<strong>Note :</strong> Afin quune recherche par adresse courriel trouve "
"des clés, vous devez les téléverser individuellement."
#: src/gettext_strings.rs:53
msgid "Verifying your email address…"
msgstr "Confirmation de votre adresse courriel…"
#: src/gettext_strings.rs:54
msgid ""
"If the process doesn't complete after a few seconds, please <input type="
"\"submit\" class=\"textbutton\" value=\"click here\" />."
@@ -369,15 +288,12 @@ msgstr ""
"Si le processus nest pas terminé après quelques secondes, veuillez <input "
"type=\"submit\" class=\"textbutton\" value=\"cliquer ici\"/>."
#: src/gettext_strings.rs:56
msgid "Manage your key on {{domain}}"
msgstr "Gérer votre clé sur {{domain}}"
#: src/gettext_strings.rs:58
msgid "Hi,"
msgstr "Bonjour,"
#: src/gettext_strings.rs:59
msgid ""
"This is an automated message from <a href=\"{{base_uri}}\" style=\"text-"
"decoration:none; color: #333\">{{domain}}</a>."
@@ -385,15 +301,12 @@ msgstr ""
"Ceci est un courriel automatisé de <a href=\"{{base_uri}}\" style=\"text-"
"decoration:none; color: #333\">{{domain}}</a>."
#: src/gettext_strings.rs:60
msgid "If you didn't request this message, please ignore it."
msgstr "Si vous navez pas demandé ce courriel, veuillez lignorer."
#: src/gettext_strings.rs:61
msgid "OpenPGP key: <tt>{{primary_fp}}</tt>"
msgstr "Clé OpenPGP : <tt>{{primary_fp}}</tt>"
#: src/gettext_strings.rs:62
msgid ""
"To manage and delete listed addresses on this key, please follow the link "
"below:"
@@ -401,7 +314,6 @@ msgstr ""
"Pour gérer et supprimer les adresses répertoriées de cette clé, veuillez "
"suivre le lien ci-dessous :"
#: src/gettext_strings.rs:63
msgid ""
"You can find more info at <a href=\"{{base_uri}}/about\">{{domain}}/about</"
"a>."
@@ -409,27 +321,21 @@ msgstr ""
"Pour de plus amples renseignements, consultez <a href=\"{{base_uri}}/about"
"\">{{domain}}/about</a>."
#: src/gettext_strings.rs:64
msgid "distributing OpenPGP keys since 2019"
msgstr "distribue des clés OpenPGP depuis 2019"
#: src/gettext_strings.rs:67
msgid "This is an automated message from {{domain}}."
msgstr "Ceci est un courriel automatisé de {{domain}}."
#: src/gettext_strings.rs:69
msgid "OpenPGP key: {{primary_fp}}"
msgstr "Clé OpenPGP : {{primary_fp}}"
#: src/gettext_strings.rs:71
msgid "You can find more info at {{base_uri}}/about"
msgstr "Pour de plus amples renseignements, consultez {{base_uri}}/about"
#: src/gettext_strings.rs:74
msgid "Verify {{userid}} for your key on {{domain}}"
msgstr "Confirmer {{userid}} pour votre clé sur {{domain}}"
#: src/gettext_strings.rs:80
msgid ""
"To let others find this key from your email address \"<a rel=\"nofollow\" "
"href=\"#\" style=\"text-decoration:none; color: #333\">{{userid}}</a>\", "
@@ -439,7 +345,6 @@ msgstr ""
"courriel « <a rel=\"nofollow\" href=\"#\" style=\"text-decoration:none; "
"color : #333\">{{userid}}</a> », veuillez cliquer sur le lien ci-dessous :"
#: src/gettext_strings.rs:88
msgid ""
"To let others find this key from your email address \"{{userid}}\",\n"
"please follow the link below:"
@@ -447,42 +352,57 @@ msgstr ""
"Afin de permettre à dautres de trouver cette clé à partir de votre\n"
"adresse courriel « {{userid}} », veuillez suivre le lien ci-dessous :"
#: src/web/manage.rs:103
msgid "No key found for fingerprint {}"
msgstr "Aucune clé na été trouvée pour lempreinte {}"
msgid "No key found for key id {}"
msgstr "Aucune clé na été trouvée pour lID de clé {}"
msgid "No key found for email address {}"
msgstr "Aucune clé na été trouvée pour ladresse courriel {}"
msgid "Search by Short Key ID is not supported."
msgstr "La recherche par ID de clé courte nest pas prise en charge."
msgid "Invalid search query."
msgstr "La requête dinterrogation est invalide."
msgctxt "Subject for verification email, {0} = userid, {1} = keyserver domain"
msgid "Verify {0} for your key on {1}"
msgstr "Confirmer {0} pour votre clé sur {1}"
msgctxt "Subject for manage email, {} = keyserver domain"
msgid "Manage your key on {}"
msgstr "Gérer votre clé sur {}"
msgid "This link is invalid or expired"
msgstr "Ce lien est invalide ou expiré"
#: src/web/manage.rs:129
msgid "Malformed address: {address}"
#, fuzzy
msgid "Malformed address: {}"
msgstr "Cette adresse est malformée :"
#: src/web/manage.rs:136
msgid "No key for address: {address}"
#, fuzzy
msgid "No key for address: {}"
msgstr "Il ny a pas de clé pour cette adresse : {address}"
#: src/web/manage.rs:152
msgid "A request has already been sent for this address recently."
msgstr "Une demande a déjà été envoyée récemment pour cette adresse."
#: src/web/vks.rs:111
msgid "Parsing of key data failed."
msgstr "Échec danalyse des données de la clé."
#: src/web/vks.rs:120
msgid "Whoops, please don't upload secret keys!"
msgstr "Attention : Veuillez ne pas téléverser de clés secrètes!"
#: src/web/vks.rs:133
msgid "No key uploaded."
msgstr "Aucune clé na été téléversée."
#: src/web/vks.rs:177
msgid "Error processing uploaded key."
msgstr "Erreur de traitement de la clé téléversée."
#: src/web/vks.rs:247
msgid "Upload session expired. Please try again."
msgstr "La session de téléversement est expirée. Veuillez ressayer."
#: src/web/vks.rs:284
msgid "Invalid verification link."
msgstr "Le lien de confirmation est invalide."

View File

@@ -12,332 +12,250 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
#: src/mail.rs:107
msgctxt "Subject for verification email"
msgid "Verify {userid} for your key on {domain}"
msgstr ""
#: src/mail.rs:140
msgctxt "Subject for manage email"
msgid "Manage your key on {domain}"
msgstr ""
#: src/i18n_helpers.rs:8
msgid "No key found for fingerprint {fingerprint}"
msgstr ""
#: src/i18n_helpers.rs:10
msgid "No key found for key id {key_id}"
msgstr ""
#: src/i18n_helpers.rs:12
msgid "No key found for email address {email}"
msgstr ""
#: src/i18n_helpers.rs:13
msgid "Search by Short Key ID is not supported."
msgstr ""
#: src/i18n_helpers.rs:14
msgid "Invalid search query."
msgstr ""
#: src/gettext_strings.rs:4
msgid "Error"
msgstr ""
#: src/gettext_strings.rs:5
msgid "Looks like something went wrong :("
msgstr ""
#: src/gettext_strings.rs:6
msgid "Error message: {{ internal_error }}"
msgstr ""
#: src/gettext_strings.rs:7
msgid "There was an error with your request:"
msgstr ""
#: src/gettext_strings.rs:8
msgid "We found an entry for <span class=\"email\">{{ query }}</span>:"
msgstr ""
#: src/gettext_strings.rs:9
msgid "<strong>Hint:</strong> It's more convenient to use <span class=\"brand\">keys.openpgp.org</span> from your OpenPGP software.<br /> Take a look at our <a href=\"/about/usage\">usage guide</a> for details."
msgstr ""
#: src/gettext_strings.rs:10
msgid "debug info"
msgstr ""
#: src/gettext_strings.rs:11
msgid "Search by Email Address / Key ID / Fingerprint"
msgstr ""
#: src/gettext_strings.rs:12
msgid "Search"
msgstr ""
#: src/gettext_strings.rs:13
msgid "You can also <a href=\"/upload\">upload</a> or <a href=\"/manage\">manage</a> your key."
msgstr ""
#: src/gettext_strings.rs:14
msgid "Find out more <a href=\"/about\">about this service</a>."
msgstr ""
#: src/gettext_strings.rs:15
msgid "News:"
msgstr ""
#: src/gettext_strings.rs:16
msgid "<a href=\"/about/news#2019-11-12-celebrating-100k\">Celebrating 100.000 verified addresses! 📈</a> (2019-11-12)"
msgstr ""
#: src/gettext_strings.rs:17
msgid "v{{ version }} built from"
msgstr ""
#: src/gettext_strings.rs:18
msgid "Powered by <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>"
msgstr ""
#: src/gettext_strings.rs:19
msgid "Background image retrieved from <a href=\"https://www.toptal.com/designers/subtlepatterns/subtle-grey/\">Subtle Patterns</a> under CC BY-SA 3.0"
msgstr ""
#: src/gettext_strings.rs:20
msgid "Maintenance Mode"
msgstr ""
#: src/gettext_strings.rs:21
msgid "Manage your key"
msgstr ""
#: src/gettext_strings.rs:22
msgid "Enter any verified email address for your key"
msgstr ""
#: src/gettext_strings.rs:23
msgid "Send link"
msgstr ""
#: src/gettext_strings.rs:24
msgid "We will send you an email with a link you can use to remove any of your email addresses from search."
msgstr ""
#: src/gettext_strings.rs:25
msgid "Managing the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" target=\"_blank\">{{ key_fpr }}</a></span>."
msgstr ""
#: src/gettext_strings.rs:26
msgid "Your key is published with the following identity information:"
msgstr ""
#: src/gettext_strings.rs:27
msgid "Delete"
msgstr ""
#: src/gettext_strings.rs:28
msgid "Clicking \"delete\" on any address will remove it from this key. It will no longer appear in a search.<br /> To add another address, <a href=\"/upload\">upload</a> the key again."
msgstr ""
#: src/gettext_strings.rs:29
msgid "Your key is published as only non-identity information. (<a href=\"/about\" target=\"_blank\">What does this mean?</a>)"
msgstr ""
#: src/gettext_strings.rs:30
msgid "To add an address, <a href=\"/upload\">upload</a> the key again."
msgstr ""
#: src/gettext_strings.rs:31
msgid "We have sent an email with further instructions to <span class=\"email\">{{ address }}</span>."
msgstr ""
#: src/gettext_strings.rs:32
msgid "This address has already been verified."
msgstr ""
#: src/gettext_strings.rs:33
msgid "Your key <span class=\"fingerprint\">{{ key_fpr }}</span> is now published for the identity <a href=\"{{userid_link}}\" target=\"_blank\"><span class=\"email\">{{ userid }}</span></a>."
msgstr ""
#: src/gettext_strings.rs:34
msgid "Upload your key"
msgstr ""
#: src/gettext_strings.rs:35
msgid "Upload"
msgstr ""
#: src/gettext_strings.rs:36
msgid "Need more info? Check our <a target=\"_blank\" href=\"/about\">intro</a> and <a target=\"_blank\" href=\"/about/usage\">usage guide</a>."
msgstr ""
#: src/gettext_strings.rs:37
msgid "You uploaded the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" target=\"_blank\">{{ key_fpr }}</a></span>."
msgstr ""
#: src/gettext_strings.rs:38
msgid "This key is revoked."
msgstr ""
#: src/gettext_strings.rs:39
msgid "It is published without identity information and can't be made available for search by email address (<a href=\"/about\" target=\"_blank\">what does this mean?</a>)."
msgstr ""
#: src/gettext_strings.rs:40
msgid "This key is now published with the following identity information (<a href=\"/about\" target=\"_blank\">what does this mean?</a>):"
msgstr ""
#: src/gettext_strings.rs:41
msgid "Published"
msgstr ""
#: src/gettext_strings.rs:42
msgid "This key is now published with only non-identity information. (<a href=\"/about\" target=\"_blank\">What does this mean?</a>)"
msgstr ""
#: src/gettext_strings.rs:43
msgid "To make the key available for search by email address, you can verify it belongs to you:"
msgstr ""
#: src/gettext_strings.rs:44
msgid "Verification Pending"
msgstr ""
#: src/gettext_strings.rs:45
msgid "<strong>Note:</strong> Some providers delay emails for up to 15 minutes to prevent spam. Please be patient."
msgstr ""
#: src/gettext_strings.rs:46
msgid "Send Verification Email"
msgstr ""
#: src/gettext_strings.rs:47
msgid "This key contains one identity that could not be parsed as an email address.<br /> This identity can't be published on <span class=\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-email-uids\" target=\"_blank\">Why?</a>)"
msgstr ""
#: src/gettext_strings.rs:48
msgid "This key contains {{ count_unparsed }} identities that could not be parsed as an email address.<br /> These identities can't be published on <span class=\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-email-uids\" target=\"_blank\">Why?</a>)"
msgstr ""
#: src/gettext_strings.rs:49
msgid "This key contains one revoked identity, which is not published. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
msgstr ""
#: src/gettext_strings.rs:50
msgid "This key contains {{ count_revoked }} revoked identities, which are not published. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
msgstr ""
#: src/gettext_strings.rs:51
msgid "Your keys have been successfully uploaded:"
msgstr ""
#: src/gettext_strings.rs:52
msgid "<strong>Note:</strong> To make keys searchable by email address, you must upload them individually."
msgstr ""
#: src/gettext_strings.rs:53
msgid "Verifying your email address…"
msgstr ""
#: src/gettext_strings.rs:54
msgid "If the process doesn't complete after a few seconds, please <input type=\"submit\" class=\"textbutton\" value=\"click here\" />."
msgstr ""
#: src/gettext_strings.rs:56
msgid "Manage your key on {{domain}}"
msgstr ""
#: src/gettext_strings.rs:58
msgid "Hi,"
msgstr ""
#: src/gettext_strings.rs:59
msgid "This is an automated message from <a href=\"{{base_uri}}\" style=\"text-decoration:none; color: #333\">{{domain}}</a>."
msgstr ""
#: src/gettext_strings.rs:60
msgid "If you didn't request this message, please ignore it."
msgstr ""
#: src/gettext_strings.rs:61
msgid "OpenPGP key: <tt>{{primary_fp}}</tt>"
msgstr ""
#: src/gettext_strings.rs:62
msgid "To manage and delete listed addresses on this key, please follow the link below:"
msgstr ""
#: src/gettext_strings.rs:63
msgid "You can find more info at <a href=\"{{base_uri}}/about\">{{domain}}/about</a>."
msgstr ""
#: src/gettext_strings.rs:64
msgid "distributing OpenPGP keys since 2019"
msgstr ""
#: src/gettext_strings.rs:67
msgid "This is an automated message from {{domain}}."
msgstr ""
#: src/gettext_strings.rs:69
msgid "OpenPGP key: {{primary_fp}}"
msgstr ""
#: src/gettext_strings.rs:71
msgid "You can find more info at {{base_uri}}/about"
msgstr ""
#: src/gettext_strings.rs:74
msgid "Verify {{userid}} for your key on {{domain}}"
msgstr ""
#: src/gettext_strings.rs:80
msgid "To let others find this key from your email address \"<a rel=\"nofollow\" href=\"#\" style=\"text-decoration:none; color: #333\">{{userid}}</a>\", please click the link below:"
msgstr ""
#: src/gettext_strings.rs:88
msgid "To let others find this key from your email address \"{{userid}}\",\nplease follow the link below:"
msgstr ""
#: src/web/manage.rs:103
msgid "No key found for fingerprint {}"
msgstr ""
msgid "No key found for key id {}"
msgstr ""
msgid "No key found for email address {}"
msgstr ""
msgid "Search by Short Key ID is not supported."
msgstr ""
msgid "Invalid search query."
msgstr ""
msgctxt "Subject for verification email, {0} = userid, {1} = keyserver domain"
msgid "Verify {0} for your key on {1}"
msgstr ""
msgctxt "Subject for manage email, {} = keyserver domain"
msgid "Manage your key on {}"
msgstr ""
msgid "This link is invalid or expired"
msgstr ""
#: src/web/manage.rs:129
msgid "Malformed address: {address}"
msgid "Malformed address: {}"
msgstr ""
#: src/web/manage.rs:136
msgid "No key for address: {address}"
msgid "No key for address: {}"
msgstr ""
#: src/web/manage.rs:152
msgid "A request has already been sent for this address recently."
msgstr ""
#: src/web/vks.rs:111
msgid "Parsing of key data failed."
msgstr ""
#: src/web/vks.rs:120
msgid "Whoops, please don't upload secret keys!"
msgstr ""
#: src/web/vks.rs:133
msgid "No key uploaded."
msgstr ""
#: src/web/vks.rs:177
msgid "Error processing uploaded key."
msgstr ""
#: src/web/vks.rs:247
msgid "Upload session expired. Please try again."
msgstr ""
#: src/web/vks.rs:284
msgid "Invalid verification link."
msgstr ""

View File

@@ -15,58 +15,21 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: src/mail.rs:107
msgctxt "Subject for verification email"
msgid "Verify {userid} for your key on {domain}"
msgstr "Verifica {userid} per la tua chiave su {domain}"
#: src/mail.rs:140
msgctxt "Subject for manage email"
msgid "Manage your key on {domain}"
msgstr "Gestisci la tua chiave su {domain}"
#: src/i18n_helpers.rs:8
msgid "No key found for fingerprint {fingerprint}"
msgstr ""
#: src/i18n_helpers.rs:10
msgid "No key found for key id {key_id}"
msgstr ""
#: src/i18n_helpers.rs:12
#, fuzzy
msgid "No key found for email address {email}"
msgstr "Nessuna chiave per l'indirizzo: {address}"
#: src/i18n_helpers.rs:13
msgid "Search by Short Key ID is not supported."
msgstr ""
#: src/i18n_helpers.rs:14
msgid "Invalid search query."
msgstr ""
#: src/gettext_strings.rs:4
msgid "Error"
msgstr "Errore"
#: src/gettext_strings.rs:5
msgid "Looks like something went wrong :("
msgstr "Sembra che qualcosa sia andato storto :("
#: src/gettext_strings.rs:6
msgid "Error message: {{ internal_error }}"
msgstr "Messaggio di errore: {{ internal_error }}"
#: src/gettext_strings.rs:7
msgid "There was an error with your request:"
msgstr "Si è verificato un errore con la tua richiesta:"
#: src/gettext_strings.rs:8
msgid "We found an entry for <span class=\"email\">{{ query }}</span>:"
msgstr "Abbiamo trovato una voce per <span class=\"email\">{{ query }}</span>:"
#: src/gettext_strings.rs:9
msgid ""
"<strong>Hint:</strong> It's more convenient to use <span class=\"brand"
"\">keys.openpgp.org</span> from your OpenPGP software.<br /> Take a look at "
@@ -76,19 +39,15 @@ msgstr ""
"\">keys.opepgp.org</span>dalla tua installazione di OpenPGP. <br />Consulta "
"la nostra <a href=\"/about/usage\">guida all'uso</a> per i dettagli."
#: src/gettext_strings.rs:10
msgid "debug info"
msgstr "informazioni di debug"
#: src/gettext_strings.rs:11
msgid "Search by Email Address / Key ID / Fingerprint"
msgstr "Cerca per indirizzo email / ID Chiave / Fingerprint"
#: src/gettext_strings.rs:12
msgid "Search"
msgstr "Cerca"
#: src/gettext_strings.rs:13
msgid ""
"You can also <a href=\"/upload\">upload</a> or <a href=\"/manage\">manage</"
"a> your key."
@@ -96,29 +55,23 @@ msgstr ""
"Puoi anche <a href=\"/upload\">caricare</a> o <a href=\"/manage\">gestire</"
"a> una tua chiave."
#: src/gettext_strings.rs:14
msgid "Find out more <a href=\"/about\">about this service</a>."
msgstr "Scopri di più <a href=\"/about\">su questo servizio</a>."
#: src/gettext_strings.rs:15
msgid "News:"
msgstr "Novità:"
#: src/gettext_strings.rs:16
msgid ""
"<a href=\"/about/news#2019-11-12-celebrating-100k\">Celebrating 100.000 "
"verified addresses! 📈</a> (2019-11-12)"
msgstr ""
#: src/gettext_strings.rs:17
msgid "v{{ version }} built from"
msgstr "v{{ version }} compilata da "
#: src/gettext_strings.rs:18
msgid "Powered by <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>"
msgstr "Fatto con <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>"
#: src/gettext_strings.rs:19
msgid ""
"Background image retrieved from <a href=\"https://www.toptal.com/designers/"
"subtlepatterns/subtle-grey/\">Subtle Patterns</a> under CC BY-SA 3.0"
@@ -126,23 +79,18 @@ msgstr ""
"Immagine di sfondo ottenuta da <a href=\"https://www.toptal.com/designers/"
"subtlepatterns/subtle-grey/\">Subtle Patterns</a> secondo CC BY-SA 3.0"
#: src/gettext_strings.rs:20
msgid "Maintenance Mode"
msgstr "Modalità Manutenzione"
#: src/gettext_strings.rs:21
msgid "Manage your key"
msgstr "Gestisci la tua chiave"
#: src/gettext_strings.rs:22
msgid "Enter any verified email address for your key"
msgstr "Inserisci un qualsiasi indirizzo email verificato per la tua chiave"
#: src/gettext_strings.rs:23
msgid "Send link"
msgstr "Invia link"
#: src/gettext_strings.rs:24
msgid ""
"We will send you an email with a link you can use to remove any of your "
"email addresses from search."
@@ -150,7 +98,6 @@ msgstr ""
"Ti manderemo una email con un link, che potrai usare per rimuovere uno dei "
"tuoi indirizzi email dai risultati della ricerca"
#: src/gettext_strings.rs:25
msgid ""
"Managing the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
@@ -158,16 +105,13 @@ msgstr ""
"Stai gestendo la chiave <span class=\"fingerprint\"><a href="
"\"{{ key_link }}\" target=\"_blank\">{{ key_fpr }}</a></span>."
#: src/gettext_strings.rs:26
msgid "Your key is published with the following identity information:"
msgstr ""
"La tua chiave è stata pubblicata con le seguenti informazioni identificative:"
#: src/gettext_strings.rs:27
msgid "Delete"
msgstr "Rimuovi"
#: src/gettext_strings.rs:28
msgid ""
"Clicking \"delete\" on any address will remove it from this key. It will no "
"longer appear in a search.<br /> To add another address, <a href=\"/upload"
@@ -177,7 +121,6 @@ msgstr ""
"chiave. Non apparirà più nelle ricerche.<br />Per aggiungere un altro "
"indirizzo, <a href=\"/upload\">carica</a> la chiave nuovamente."
#: src/gettext_strings.rs:29
msgid ""
"Your key is published as only non-identity information. (<a href=\"/about\" "
"target=\"_blank\">What does this mean?</a>)"
@@ -185,13 +128,11 @@ msgstr ""
"La tua chiave è pubblicata solo con informazioni non identificative. (<a "
"href=\"/about\" target=\"_blank\">Cosa vuol dire?</a>)"
#: src/gettext_strings.rs:30
msgid "To add an address, <a href=\"/upload\">upload</a> the key again."
msgstr ""
"Per aggiungere un indirizzo, <a href=\"/upload\">carica</a> la chiave "
"nuovamente."
#: src/gettext_strings.rs:31
msgid ""
"We have sent an email with further instructions to <span class=\"email"
"\">{{ address }}</span>."
@@ -199,11 +140,9 @@ msgstr ""
"Abbiamo inviato una email con ulteriori istruzioni a <span class=\"email"
"\">{{ address }}</span>."
#: src/gettext_strings.rs:32
msgid "This address has already been verified."
msgstr "L'indirizzo è già stato verificato."
#: src/gettext_strings.rs:33
msgid ""
"Your key <span class=\"fingerprint\">{{ key_fpr }}</span> is now published "
"for the identity <a href=\"{{userid_link}}\" target=\"_blank\"><span class="
@@ -213,15 +152,12 @@ msgstr ""
"pubblicata per l'indirizzo <a href=\"{{userid_link}}\" target=\"_blank"
"\"><span class=\"email\">{{ userid }}</span></a>."
#: src/gettext_strings.rs:34
msgid "Upload your key"
msgstr "Carica la tua chiave"
#: src/gettext_strings.rs:35
msgid "Upload"
msgstr "Carica"
#: src/gettext_strings.rs:36
msgid ""
"Need more info? Check our <a target=\"_blank\" href=\"/about\">intro</a> and "
"<a target=\"_blank\" href=\"/about/usage\">usage guide</a>."
@@ -230,7 +166,6 @@ msgstr ""
"about\">introduzione</a> e <a target=\"_blank\" href=\"/about/usage\">guida "
"all'uso</a>."
#: src/gettext_strings.rs:37
msgid ""
"You uploaded the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
@@ -238,11 +173,9 @@ msgstr ""
"Hai caricato la chiave <span class=\"fingerprint\"><a href="
"\"{{ key_link }}\" target=\"_blank\">{{ key_fpr }}</a></span>."
#: src/gettext_strings.rs:38
msgid "This key is revoked."
msgstr "La chiave è stata revocata."
#: src/gettext_strings.rs:39
msgid ""
"It is published without identity information and can't be made available for "
"search by email address (<a href=\"/about\" target=\"_blank\">what does this "
@@ -252,7 +185,6 @@ msgstr ""
"disponibile per la ricerca per indirizzo email (<a href=\"/about\" target="
"\"_blank\">Che cosa vuol dire?</a>)."
#: src/gettext_strings.rs:40
msgid ""
"This key is now published with the following identity information (<a href="
"\"/about\" target=\"_blank\">what does this mean?</a>):"
@@ -260,11 +192,9 @@ msgstr ""
"La chiave è ora pubblicata con le seguenti informazioni identificative (<a "
"href=\"/about\" target=\"_blank\">Cosa vuol dire?</a>):"
#: src/gettext_strings.rs:41
msgid "Published"
msgstr "Pubblicato"
#: src/gettext_strings.rs:42
msgid ""
"This key is now published with only non-identity information. (<a href=\"/"
"about\" target=\"_blank\">What does this mean?</a>)"
@@ -272,7 +202,6 @@ msgstr ""
"La tua chiave è pubblicata solo con informazioni non identificative. (<a "
"href=\"/about\" target=\"_blank\">Cosa vuol dire?</a>)"
#: src/gettext_strings.rs:43
msgid ""
"To make the key available for search by email address, you can verify it "
"belongs to you:"
@@ -280,11 +209,9 @@ msgstr ""
"Per rendere la chiave disponibile nelle richerche per indirizzo email, puoi "
"verificarne il possesso:"
#: src/gettext_strings.rs:44
msgid "Verification Pending"
msgstr "In attesa della verifica"
#: src/gettext_strings.rs:45
msgid ""
"<strong>Note:</strong> Some providers delay emails for up to 15 minutes to "
"prevent spam. Please be patient."
@@ -292,11 +219,9 @@ msgstr ""
"<strong>Nota:</strong> Alcuni provider ritardano le email fino a 15 minuti "
"per prevenire lo spam. Si prega di attendere."
#: src/gettext_strings.rs:46
msgid "Send Verification Email"
msgstr "Invia email di verifica"
#: src/gettext_strings.rs:47
msgid ""
"This key contains one identity that could not be parsed as an email address."
"<br /> This identity can't be published on <span class=\"brand\">keys."
@@ -308,7 +233,6 @@ msgstr ""
"class=\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-email-uids"
"\" target=\"_blank\">Perché?</a>)"
#: src/gettext_strings.rs:48
msgid ""
"This key contains {{ count_unparsed }} identities that could not be parsed "
"as an email address.<br /> These identities can't be published on <span "
@@ -320,7 +244,6 @@ msgstr ""
"pubblicate su <span class=\"brand\">keys.openpgp.org</span>. (<a href=\"/"
"about/faq#non-email-uids\" target=\"_blank\">Perché?</a>)"
#: src/gettext_strings.rs:49
msgid ""
"This key contains one revoked identity, which is not published. (<a href=\"/"
"about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
@@ -328,7 +251,6 @@ msgstr ""
"Questa chiave contiene una identità revocata, che non è pubblicata. (<a href="
"\"/about/faq#revoked-uids\" target=\"_blank\">Perché?</a>)"
#: src/gettext_strings.rs:50
msgid ""
"This key contains {{ count_revoked }} revoked identities, which are not "
"published. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
@@ -337,11 +259,9 @@ msgstr ""
"pubblicate. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Perché?</"
"a>)"
#: src/gettext_strings.rs:51
msgid "Your keys have been successfully uploaded:"
msgstr "Le tue chiavi sono state caricate con successo:"
#: src/gettext_strings.rs:52
msgid ""
"<strong>Note:</strong> To make keys searchable by email address, you must "
"upload them individually."
@@ -349,11 +269,9 @@ msgstr ""
"<strong>Nota:</strong> Per rendere le chiavi disponibili alla ricerca per "
"indirizzo email, devi caricarle singolarmente."
#: src/gettext_strings.rs:53
msgid "Verifying your email address…"
msgstr "Stiamo verificando il tuo indirizzo email..."
#: src/gettext_strings.rs:54
msgid ""
"If the process doesn't complete after a few seconds, please <input type="
"\"submit\" class=\"textbutton\" value=\"click here\" />."
@@ -361,15 +279,12 @@ msgstr ""
"Se il processo non termina dopo qualche secondo, siete pregati di <input "
"type=\"submit\" class=\"textbutton\" value=\"cliccare qui\"/>"
#: src/gettext_strings.rs:56
msgid "Manage your key on {{domain}}"
msgstr "Gestisci la tua chiave su {{domain}}"
#: src/gettext_strings.rs:58
msgid "Hi,"
msgstr "Ciao,"
#: src/gettext_strings.rs:59
msgid ""
"This is an automated message from <a href=\"{{base_uri}}\" style=\"text-"
"decoration:none; color: #333\">{{domain}}</a>."
@@ -377,15 +292,12 @@ msgstr ""
"Questo è un messaggio automatico da <a href=\"{{base_uri}}\" style=\"text-"
"decoration:none; color: #333\">{{domain}}</a>."
#: src/gettext_strings.rs:60
msgid "If you didn't request this message, please ignore it."
msgstr "Se non hai richiesto questo messaggio, sei pregato di ignorarlo."
#: src/gettext_strings.rs:61
msgid "OpenPGP key: <tt>{{primary_fp}}</tt>"
msgstr "Chiave OpenPGP: {{primary_fp}}"
#: src/gettext_strings.rs:62
msgid ""
"To manage and delete listed addresses on this key, please follow the link "
"below:"
@@ -393,7 +305,6 @@ msgstr ""
"Per gestire e cancellare gli indirizzi associati a questa chiave, usa il "
"link sottostante:"
#: src/gettext_strings.rs:63
msgid ""
"You can find more info at <a href=\"{{base_uri}}/about\">{{domain}}/about</"
"a>."
@@ -401,27 +312,21 @@ msgstr ""
"Puoi trovare più informazioni all'indirizzo <a href=\"{{base_uri}}/about"
"\">{{domain}}/about</a>."
#: src/gettext_strings.rs:64
msgid "distributing OpenPGP keys since 2019"
msgstr "Distribuiamo chiavi OpenPGP dal 2019"
#: src/gettext_strings.rs:67
msgid "This is an automated message from {{domain}}."
msgstr "Questo è un messaggio inviato automaticamente da {{domain}}."
#: src/gettext_strings.rs:69
msgid "OpenPGP key: {{primary_fp}}"
msgstr "Chiave OpenPGP: {{primary_fp}}"
#: src/gettext_strings.rs:71
msgid "You can find more info at {{base_uri}}/about"
msgstr "Puoi trovare più informazioni all'indirizzo {{base_uri}}/about"
#: src/gettext_strings.rs:74
msgid "Verify {{userid}} for your key on {{domain}}"
msgstr "Verifica {{userid}} per la tua chiave su {{domain}}"
#: src/gettext_strings.rs:80
msgid ""
"To let others find this key from your email address \"<a rel=\"nofollow\" "
"href=\"#\" style=\"text-decoration:none; color: #333\">{{userid}}</a>\", "
@@ -431,7 +336,6 @@ msgstr ""
"\"<a rel=\"nofollow\" href=\"#\" style=\"text-decoration:none; color: "
"#333\">{{userid}}</a>\", vai al link sottostante:"
#: src/gettext_strings.rs:88
msgid ""
"To let others find this key from your email address \"{{userid}}\",\n"
"please follow the link below:"
@@ -439,43 +343,58 @@ msgstr ""
"Per consentire agli altri di trovare questa chiave dal tuo indirizzo email "
"\"{{userid}}\", vai al link sottostante:"
#: src/web/manage.rs:103
msgid "No key found for fingerprint {}"
msgstr "Nessuna chiave per l'indirizzo: {}"
msgid "No key found for key id {}"
msgstr "Nessuna chiave per l'indirizzo: {}"
msgid "No key found for email address {}"
msgstr "Nessuna chiave per l'indirizzo: {}"
msgid "Search by Short Key ID is not supported."
msgstr ""
msgid "Invalid search query."
msgstr ""
msgctxt "Subject for verification email, {0} = userid, {1} = keyserver domain"
msgid "Verify {0} for your key on {1}"
msgstr "Verifica {0} per la tua chiave su {1}"
msgctxt "Subject for manage email, {} = keyserver domain"
msgid "Manage your key on {}"
msgstr "Gestisci la tua chiave su {}"
msgid "This link is invalid or expired"
msgstr "Questo link non è valido o è scaduto"
#: src/web/manage.rs:129
msgid "Malformed address: {address}"
msgstr "Indirizzo non valido: {address}"
#, fuzzy
msgid "Malformed address: {}"
msgstr "Indirizzo non valido: {}"
#: src/web/manage.rs:136
msgid "No key for address: {address}"
msgstr "Nessuna chiave per l'indirizzo: {address}"
#, fuzzy
msgid "No key for address: {}"
msgstr "Nessuna chiave per l'indirizzo: {}"
#: src/web/manage.rs:152
msgid "A request has already been sent for this address recently."
msgstr "Una richiesta per questo indirizzo è già stata inviata recentemente."
#: src/web/vks.rs:111
msgid "Parsing of key data failed."
msgstr "Impossibile elaborare i dati della chiave."
#: src/web/vks.rs:120
msgid "Whoops, please don't upload secret keys!"
msgstr "Ooops, non caricare chiavi segrete!"
#: src/web/vks.rs:133
msgid "No key uploaded."
msgstr "Nessuna chaive caricata."
#: src/web/vks.rs:177
msgid "Error processing uploaded key."
msgstr "Errore nell'elaborazione della chiave caricata."
#: src/web/vks.rs:247
msgid "Upload session expired. Please try again."
msgstr "Sessione di caricamento scaduta. Si prega di riprovare."
#: src/web/vks.rs:284
msgid "Invalid verification link."
msgstr "Link di verifica non valido."

View File

@@ -15,59 +15,22 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#: src/mail.rs:107
msgctxt "Subject for verification email"
msgid "Verify {userid} for your key on {domain}"
msgstr " {domain}のあなたの鍵のために{userid}を検証する"
#: src/mail.rs:140
msgctxt "Subject for manage email"
msgid "Manage your key on {domain}"
msgstr "{domain}の鍵を管理する"
#: src/i18n_helpers.rs:8
msgid "No key found for fingerprint {fingerprint}"
msgstr ""
#: src/i18n_helpers.rs:10
msgid "No key found for key id {key_id}"
msgstr ""
#: src/i18n_helpers.rs:12
#, fuzzy
msgid "No key found for email address {email}"
msgstr "このアドレスに対する鍵がありません: {address}"
#: src/i18n_helpers.rs:13
msgid "Search by Short Key ID is not supported."
msgstr ""
#: src/i18n_helpers.rs:14
msgid "Invalid search query."
msgstr ""
#: src/gettext_strings.rs:4
msgid "Error"
msgstr "エラー"
#: src/gettext_strings.rs:5
msgid "Looks like something went wrong :("
msgstr "なにかがうまくいかないようです :("
#: src/gettext_strings.rs:6
msgid "Error message: {{ internal_error }}"
msgstr "エラーメッセージ: {{ internal_error }}"
#: src/gettext_strings.rs:7
msgid "There was an error with your request:"
msgstr "あなたのリクエストにエラーがありました。"
#: src/gettext_strings.rs:8
msgid "We found an entry for <span class=\"email\">{{ query }}</span>:"
msgstr ""
"<span class=\"email\">{{ query }}</span>に対して一つのエントリをみつけました:"
#: src/gettext_strings.rs:9
msgid ""
"<strong>Hint:</strong> It's more convenient to use <span class=\"brand"
"\">keys.openpgp.org</span> from your OpenPGP software.<br /> Take a look at "
@@ -77,19 +40,15 @@ msgstr ""
"のOpenPGPソフトウェアから使うのがより便利です。<br /> 詳しくは、わたしたちの"
"<a href=\"/about/usage\">使い方のガイド</a>をご覧ください。"
#: src/gettext_strings.rs:10
msgid "debug info"
msgstr "デバッグ情報"
#: src/gettext_strings.rs:11
msgid "Search by Email Address / Key ID / Fingerprint"
msgstr "メールアドレス / 鍵ID / フィンガープリントで検索する。"
#: src/gettext_strings.rs:12
msgid "Search"
msgstr "検索"
#: src/gettext_strings.rs:13
msgid ""
"You can also <a href=\"/upload\">upload</a> or <a href=\"/manage\">manage</"
"a> your key."
@@ -97,29 +56,23 @@ msgstr ""
"あなたの鍵を<a href=\"/upload\">アップロード</a>または<a href=\"/manage\">管"
"理</a>もできます。"
#: src/gettext_strings.rs:14
msgid "Find out more <a href=\"/about\">about this service</a>."
msgstr "<a href=\"/about\">このサービスについて</a>さらにくわしく。"
#: src/gettext_strings.rs:15
msgid "News:"
msgstr "ニュース:"
#: src/gettext_strings.rs:16
msgid ""
"<a href=\"/about/news#2019-11-12-celebrating-100k\">Celebrating 100.000 "
"verified addresses! 📈</a> (2019-11-12)"
msgstr ""
#: src/gettext_strings.rs:17
msgid "v{{ version }} built from"
msgstr "v{{ version }} 、以下でビルトされました"
#: src/gettext_strings.rs:18
msgid "Powered by <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>"
msgstr "<a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>で動いています"
#: src/gettext_strings.rs:19
msgid ""
"Background image retrieved from <a href=\"https://www.toptal.com/designers/"
"subtlepatterns/subtle-grey/\">Subtle Patterns</a> under CC BY-SA 3.0"
@@ -127,31 +80,25 @@ msgstr ""
"背景の画像は<a href=\"https://www.toptal.com/designers/subtlepatterns/subtle-"
"grey/\">Subtle Patterns</a> からCC BY-SA 3.0のもとで取得されました。"
#: src/gettext_strings.rs:20
msgid "Maintenance Mode"
msgstr "メンテナンスモード"
#: src/gettext_strings.rs:21
msgid "Manage your key"
msgstr "あなたの鍵を管理する"
#: src/gettext_strings.rs:22
msgid "Enter any verified email address for your key"
msgstr ""
"あなたの鍵に対して検証されたメールアドレスをどれでもよいので入力してください"
#: src/gettext_strings.rs:23
msgid "Send link"
msgstr "リンクを送る"
#: src/gettext_strings.rs:24
msgid ""
"We will send you an email with a link you can use to remove any of your "
"email addresses from search."
msgstr ""
"検索からあなたのメールアドレスを削除するためのリンクのメールを送ります。"
#: src/gettext_strings.rs:25
msgid ""
"Managing the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
@@ -159,15 +106,12 @@ msgstr ""
"鍵<span class=\"fingerprint\"><a href=\"{{ key_link }}\" target=\"_blank"
"\">{{ key_fpr }}</a></span>を管理する。"
#: src/gettext_strings.rs:26
msgid "Your key is published with the following identity information:"
msgstr "以下のアイデンティティ情報であなたの鍵は公開されました。"
#: src/gettext_strings.rs:27
msgid "Delete"
msgstr "削除する"
#: src/gettext_strings.rs:28
msgid ""
"Clicking \"delete\" on any address will remove it from this key. It will no "
"longer appear in a search.<br /> To add another address, <a href=\"/upload"
@@ -177,7 +121,6 @@ msgstr ""
"索に現れなくなります。<br />アドレスを加えるにはその鍵を再度<a href=\"/upload"
"\">アップロード</a>してください。"
#: src/gettext_strings.rs:29
msgid ""
"Your key is published as only non-identity information. (<a href=\"/about\" "
"target=\"_blank\">What does this mean?</a>)"
@@ -185,24 +128,20 @@ msgstr ""
"アイデンティティなしのみの情報であなたの鍵は公開されました。. (<a href=\"/"
"about\" target=\"_blank\">これはどういう意味?</a>)"
#: src/gettext_strings.rs:30
msgid "To add an address, <a href=\"/upload\">upload</a> the key again."
msgstr ""
"アドレスを加えるにはその鍵を再度<a href=\"/upload\">アップロード</a>してくだ"
"さい。 "
#: src/gettext_strings.rs:31
msgid ""
"We have sent an email with further instructions to <span class=\"email"
"\">{{ address }}</span>."
msgstr ""
"くわしい手順を<span class=\"email\">{{ address }}</span>にメールしました。"
#: src/gettext_strings.rs:32
msgid "This address has already been verified."
msgstr "このアドレスはすでに検証されています。"
#: src/gettext_strings.rs:33
msgid ""
"Your key <span class=\"fingerprint\">{{ key_fpr }}</span> is now published "
"for the identity <a href=\"{{userid_link}}\" target=\"_blank\"><span class="
@@ -212,15 +151,12 @@ msgstr ""
"ティティ<a href=\"{{userid_link}}\" target=\"_blank\"><span class=\"email"
"\">{{ userid }}</span></a>に対して公開されました。"
#: src/gettext_strings.rs:34
msgid "Upload your key"
msgstr "あなたの鍵をアップロードする"
#: src/gettext_strings.rs:35
msgid "Upload"
msgstr "アップロード"
#: src/gettext_strings.rs:36
msgid ""
"Need more info? Check our <a target=\"_blank\" href=\"/about\">intro</a> and "
"<a target=\"_blank\" href=\"/about/usage\">usage guide</a>."
@@ -229,7 +165,6 @@ msgstr ""
"トロ</a> と<a target=\"_blank\" href=\"/about/usage\">ユーザーガイド</a>をご"
"覧ください。"
#: src/gettext_strings.rs:37
msgid ""
"You uploaded the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
@@ -237,11 +172,9 @@ msgstr ""
"この鍵<span class=\"fingerprint\"><a href=\"{{ key_link }}\" target=\"_blank"
"\">{{ key_fpr }}</a></span>をあなたはアップロードしました。"
#: src/gettext_strings.rs:38
msgid "This key is revoked."
msgstr "この鍵はリボークされました。"
#: src/gettext_strings.rs:39
msgid ""
"It is published without identity information and can't be made available for "
"search by email address (<a href=\"/about\" target=\"_blank\">what does this "
@@ -250,7 +183,6 @@ msgstr ""
"アイデンティティの情報なしで公開され、メールアドレスによる検索は利用可能では"
"ありません(<a href=\"/about\" target=\"_blank\">どういう意味か?</a>)。"
#: src/gettext_strings.rs:40
msgid ""
"This key is now published with the following identity information (<a href="
"\"/about\" target=\"_blank\">what does this mean?</a>):"
@@ -258,11 +190,9 @@ msgstr ""
"この鍵は下記のアイデンティティ情報とともに公開されました (<a href=\"/about\" "
"target=\"_blank\">どういう意味?</a>):"
#: src/gettext_strings.rs:41
msgid "Published"
msgstr "公開されました"
#: src/gettext_strings.rs:42
msgid ""
"This key is now published with only non-identity information. (<a href=\"/"
"about\" target=\"_blank\">What does this mean?</a>)"
@@ -270,7 +200,6 @@ msgstr ""
"アイデンティティではない情報だけでこの鍵は公開されました。(<a href=\"/about"
"\" target=\"_blank\">どういう意味か?</a>)"
#: src/gettext_strings.rs:43
msgid ""
"To make the key available for search by email address, you can verify it "
"belongs to you:"
@@ -278,11 +207,9 @@ msgstr ""
"メールアドレスでの検索で鍵が利用できるようにするために、そのメールアドレスが"
"あなたのものであることを検証することができます。"
#: src/gettext_strings.rs:44
msgid "Verification Pending"
msgstr "検証を出願中です。"
#: src/gettext_strings.rs:45
msgid ""
"<strong>Note:</strong> Some providers delay emails for up to 15 minutes to "
"prevent spam. Please be patient."
@@ -290,11 +217,9 @@ msgstr ""
"<strong>注意:</strong> 迷惑メール防止のために、プロバイダは15分ほどメールを遅"
"らせることがあります。しばらくお待ちください。"
#: src/gettext_strings.rs:46
msgid "Send Verification Email"
msgstr "検証のメールを送る"
#: src/gettext_strings.rs:47
msgid ""
"This key contains one identity that could not be parsed as an email address."
"<br /> This identity can't be published on <span class=\"brand\">keys."
@@ -306,7 +231,6 @@ msgstr ""
"は公開できません。 (<a href=\"/about/faq#non-email-uids\" target=\"_blank\">"
"なぜか?</a>)"
#: src/gettext_strings.rs:48
msgid ""
"This key contains {{ count_unparsed }} identities that could not be parsed "
"as an email address.<br /> These identities can't be published on <span "
@@ -318,7 +242,6 @@ msgstr ""
"のアイデンティティは公開できません。 (<a href=\"/about/faq#non-email-uids\" "
"target=\"_blank\">なぜか?</a>)"
#: src/gettext_strings.rs:49
msgid ""
"This key contains one revoked identity, which is not published. (<a href=\"/"
"about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
@@ -326,7 +249,6 @@ msgstr ""
"この鍵はリボークされたアイデンティティが含まれており、公開されませんでした。 "
"(<a href=\"/about/faq#revoked-uids\" target=\"_blank\">なぜか?</a>)"
#: src/gettext_strings.rs:50
msgid ""
"This key contains {{ count_revoked }} revoked identities, which are not "
"published. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
@@ -335,11 +257,9 @@ msgstr ""
"が、公開されません。 (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">"
"なぜか?</a>)"
#: src/gettext_strings.rs:51
msgid "Your keys have been successfully uploaded:"
msgstr "あなたの鍵はアップロードに成功しました:"
#: src/gettext_strings.rs:52
msgid ""
"<strong>Note:</strong> To make keys searchable by email address, you must "
"upload them individually."
@@ -347,11 +267,9 @@ msgstr ""
"<strong>注意:</strong> メールアドレスで鍵を検索可能とするために、ひとつひとつ"
"独立にアップロードする必要があります。"
#: src/gettext_strings.rs:53
msgid "Verifying your email address…"
msgstr "メールアドレスを確認しています..."
#: src/gettext_strings.rs:54
msgid ""
"If the process doesn't complete after a few seconds, please <input type="
"\"submit\" class=\"textbutton\" value=\"click here\" />."
@@ -359,15 +277,12 @@ msgstr ""
"数秒後にこのプロセスが完了しない場合、どうぞこちらに<input type=\"submit\" "
"class=\"textbutton\" value=\"click here\" />お願いします。"
#: src/gettext_strings.rs:56
msgid "Manage your key on {{domain}}"
msgstr "{{domain}}のあなたの鍵を管理する"
#: src/gettext_strings.rs:58
msgid "Hi,"
msgstr "どうも、"
#: src/gettext_strings.rs:59
msgid ""
"This is an automated message from <a href=\"{{base_uri}}\" style=\"text-"
"decoration:none; color: #333\">{{domain}}</a>."
@@ -375,15 +290,12 @@ msgstr ""
"<a href=\"{{base_uri}}\" style=\"text-decoration:none; color: "
"#333\">{{domain}}</a>からの自動メッセージです。"
#: src/gettext_strings.rs:60
msgid "If you didn't request this message, please ignore it."
msgstr "このメッセージを要求していない場合、無視してください。"
#: src/gettext_strings.rs:61
msgid "OpenPGP key: <tt>{{primary_fp}}</tt>"
msgstr "OpenPGPの鍵: <tt>{{primary_fp}}</tt>"
#: src/gettext_strings.rs:62
msgid ""
"To manage and delete listed addresses on this key, please follow the link "
"below:"
@@ -391,7 +303,6 @@ msgstr ""
"この鍵の掲示されたアドレスを管理し削除するには、以下のリンクをたどってくださ"
"い:"
#: src/gettext_strings.rs:63
msgid ""
"You can find more info at <a href=\"{{base_uri}}/about\">{{domain}}/about</"
"a>."
@@ -399,27 +310,21 @@ msgstr ""
"よりくわしい情報はこちら <a href=\"{{base_uri}}/about\">{{domain}}/about</a>"
"にあります。"
#: src/gettext_strings.rs:64
msgid "distributing OpenPGP keys since 2019"
msgstr "OpenPGPの鍵を2019年から配布しています"
#: src/gettext_strings.rs:67
msgid "This is an automated message from {{domain}}."
msgstr "{{domain}}からの自動メッセージです。"
#: src/gettext_strings.rs:69
msgid "OpenPGP key: {{primary_fp}}"
msgstr "OpenPGPの鍵: {{primary_fp}}"
#: src/gettext_strings.rs:71
msgid "You can find more info at {{base_uri}}/about"
msgstr "詳しい情報はこちらにあります: {{base_uri}}/about"
#: src/gettext_strings.rs:74
msgid "Verify {{userid}} for your key on {{domain}}"
msgstr " {{domain}}に対するあなたの鍵の {{userid}}を検証する"
#: src/gettext_strings.rs:80
msgid ""
"To let others find this key from your email address \"<a rel=\"nofollow\" "
"href=\"#\" style=\"text-decoration:none; color: #333\">{{userid}}</a>\", "
@@ -429,7 +334,6 @@ msgstr ""
"decoration:none; color: #333\">{{userid}}</a>\"からこの鍵を見つけられるために"
"は、以下のリンクをクリックしてください:"
#: src/gettext_strings.rs:88
msgid ""
"To let others find this key from your email address \"{{userid}}\",\n"
"please follow the link below:"
@@ -438,42 +342,57 @@ msgstr ""
"は、\n"
"以下のリンクをたどってください:"
#: src/web/manage.rs:103
msgid "No key found for fingerprint {}"
msgstr "このアドレスに対する鍵がありません: {}"
msgid "No key found for key id {}"
msgstr "このアドレスに対する鍵がありません: {}"
msgid "No key found for email address {}"
msgstr "このアドレスに対する鍵がありません: {}"
msgid "Search by Short Key ID is not supported."
msgstr "Search by Short Key ID is not supported."
msgid "Invalid search query."
msgstr "Invalid seach query."
msgctxt "Subject for verification email, {0} = userid, {1} = keyserver domain"
msgid "Verify {0} for your key on {1}"
msgstr " {1}のあなたの鍵のために{1}を検証する"
msgctxt "Subject for manage email, {} = keyserver domain"
msgid "Manage your key on {}"
msgstr "{}の鍵を管理する"
msgid "This link is invalid or expired"
msgstr "このリンクは無効であるか失効しています"
#: src/web/manage.rs:129
msgid "Malformed address: {address}"
msgstr "形のこわれたアドレスです: {address}"
#, fuzzy
msgid "Malformed address: {}"
msgstr "形のこわれたアドレスです: {}"
#: src/web/manage.rs:136
msgid "No key for address: {address}"
msgstr "このアドレスに対する鍵がありません: {address}"
#, fuzzy
msgid "No key for address: {}"
msgstr "このアドレスに対する鍵がありません: {}"
#: src/web/manage.rs:152
msgid "A request has already been sent for this address recently."
msgstr "最近、このアドレスへリクエストがすでに送信されています。"
#: src/web/vks.rs:111
msgid "Parsing of key data failed."
msgstr "鍵データのパーズが失敗しました。"
#: src/web/vks.rs:120
msgid "Whoops, please don't upload secret keys!"
msgstr "あらら、秘密鍵をアップロードしないでください!"
#: src/web/vks.rs:133
msgid "No key uploaded."
msgstr "鍵はアップロードされませんでした。"
#: src/web/vks.rs:177
msgid "Error processing uploaded key."
msgstr "アップロードされた鍵の処理に失敗しました。"
#: src/web/vks.rs:247
msgid "Upload session expired. Please try again."
msgstr "アップロードのセッションが時間切れです。もう一度試してください。"
#: src/web/vks.rs:284
msgid "Invalid verification link."
msgstr "無効な検証のリンクです。"

View File

@@ -1,8 +1,8 @@
#
#
# Translators:
# Vincent Breitmoser <look@my.amazin.horse>, 2020
# ylsun <y15un@y15un.dog>, 2021
#
#
msgid ""
msgstr ""
"Project-Id-Version: hagrid\n"
@@ -11,348 +11,277 @@ msgstr ""
"PO-Revision-Date: 2019-09-27 18:05+0000\n"
"Last-Translator: ylsun <y15un@y15un.dog>, 2021\n"
"Language-Team: Korean (https://www.transifex.com/otf/teams/102430/ko/)\n"
"Language: ko\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: ko\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#: src/mail.rs:107
msgctxt "Subject for verification email"
msgid "Verify {userid} for your key on {domain}"
msgstr "{domain}에 {userid} 명의로 올린 키 인증"
#: src/mail.rs:140
msgctxt "Subject for manage email"
msgid "Manage your key on {domain}"
msgstr "{domain}에 올린 내 키 관리"
#: src/i18n_helpers.rs:8
msgid "No key found for fingerprint {fingerprint}"
msgstr "지문 {fingerprint}에 해당하는 키를 찾지 못했습니다."
#: src/i18n_helpers.rs:10
msgid "No key found for key id {key_id}"
msgstr "키 ID {key_id}에 해당하는 키를 찾지 못했습니다."
#: src/i18n_helpers.rs:12
msgid "No key found for email address {email}"
msgstr "전자 메일 주소 {email}에 해당하는 키를 찾지 못했습니다."
#: src/i18n_helpers.rs:13
msgid "Search by Short Key ID is not supported."
msgstr "'짧은 키 ID' 형식은 지원하지 않습니다."
#: src/i18n_helpers.rs:14
msgid "Invalid search query."
msgstr "올바르지 않은 검색어입니다."
#: src/gettext_strings.rs:4
msgid "Error"
msgstr "오류"
#: src/gettext_strings.rs:5
msgid "Looks like something went wrong :("
msgstr "뭔가 단단히 잘못된 모양입니다 :("
#: src/gettext_strings.rs:6
msgid "Error message: {{ internal_error }}"
msgstr "오류 내용: {{ internal_error }}"
#: src/gettext_strings.rs:7
msgid "There was an error with your request:"
msgstr "내 요청과 관련해 아래와 같은 문제가 발생했습니다:"
#: src/gettext_strings.rs:8
msgid "We found an entry for <span class=\"email\">{{ query }}</span>:"
msgstr "<span class=\"email\">{{ query }}</span>에 해당하는 키를 찾았습니다:"
#: src/gettext_strings.rs:9
msgid ""
"<strong>Hint:</strong> It's more convenient to use <span "
"class=\"brand\">keys.openpgp.org</span> from your OpenPGP software.<br /> "
"Take a look at our <a href=\"/about/usage\">usage guide</a> for details."
"<strong>Hint:</strong> It's more convenient to use <span class=\"brand"
"\">keys.openpgp.org</span> from your OpenPGP software.<br /> Take a look at "
"our <a href=\"/about/usage\">usage guide</a> for details."
msgstr ""
"<strong>알아두면 좋아요:</strong> <span class=\"brand\">keys.openpgp.org</span> 이용은"
" 내 OpenPGP 소프트웨어를 통해서 하는 게 더 편하답니다.<br /> 자세한 사항은 <a "
"href=\"/about/usage\">사용 안내서</a>에서 알아보세요."
"<strong>알아두면 좋아요:</strong> <span class=\"brand\">keys.openpgp.org</"
"span> 이용은 내 OpenPGP 소프트웨어를 통해서 하는 게 더 편하답니다.<br /> 자세"
"한 사항은 <a href=\"/about/usage\">사용 안내서</a>에서 알아보세요."
#: src/gettext_strings.rs:10
msgid "debug info"
msgstr "디버그 정보"
#: src/gettext_strings.rs:11
msgid "Search by Email Address / Key ID / Fingerprint"
msgstr "전자 메일 주소 / 키 ID / 지문으로 키를 찾아보세요"
#: src/gettext_strings.rs:12
msgid "Search"
msgstr "찾기"
#: src/gettext_strings.rs:13
msgid ""
"You can also <a href=\"/upload\">upload</a> or <a "
"href=\"/manage\">manage</a> your key."
msgstr "내 키를 <a href=\"/upload\">올리</a>거나 <a href=\"/manage\">관리</a>할 수도 있습니다."
"You can also <a href=\"/upload\">upload</a> or <a href=\"/manage\">manage</"
"a> your key."
msgstr ""
"내 키를 <a href=\"/upload\">올리</a>거나 <a href=\"/manage\">관리</a>할 수도 "
"있습니다."
#: src/gettext_strings.rs:14
msgid "Find out more <a href=\"/about\">about this service</a>."
msgstr "이 서비스에 대해 <a href=\"/about\">더 알아볼래요</a>."
#: src/gettext_strings.rs:15
msgid "News:"
msgstr "새 소식:"
#: src/gettext_strings.rs:16
msgid ""
"<a href=\"/about/news#2019-11-12-celebrating-100k\">Celebrating 100.000 "
"verified addresses! 📈</a> (2019-11-12)"
msgstr ""
"<a href=\"/about/news#2019-11-12-celebrating-100k\">주소 인증 100,000건을 달성했습니다! "
"📈</a> (2019-11-12)"
"<a href=\"/about/news#2019-11-12-celebrating-100k\">주소 인증 100,000건을 달"
"성했습니다! 📈</a> (2019-11-12)"
#: src/gettext_strings.rs:17
msgid "v{{ version }} built from"
msgstr "버전 {{ version }} / 빌드 커밋"
#: src/gettext_strings.rs:18
msgid "Powered by <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>"
msgstr "<a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a> 기반"
#: src/gettext_strings.rs:19
msgid ""
"Background image retrieved from <a "
"href=\"https://www.toptal.com/designers/subtlepatterns/subtle-grey/\">Subtle"
" Patterns</a> under CC BY-SA 3.0"
"Background image retrieved from <a href=\"https://www.toptal.com/designers/"
"subtlepatterns/subtle-grey/\">Subtle Patterns</a> under CC BY-SA 3.0"
msgstr ""
"배경 그림은 <a href=\"https://www.toptal.com/designers/subtlepatterns/subtle-"
"grey/\">Subtle Patterns</a>에서 가져왔고 CC BY-SA 3.0 허가서를 따릅니다"
#: src/gettext_strings.rs:20
msgid "Maintenance Mode"
msgstr "유지보수 모드"
#: src/gettext_strings.rs:21
msgid "Manage your key"
msgstr "내 키 관리"
#: src/gettext_strings.rs:22
msgid "Enter any verified email address for your key"
msgstr "내 키에 대해 인증된 전자 메일 주소를 입력하세요"
#: src/gettext_strings.rs:23
msgid "Send link"
msgstr "링크 보내기"
#: src/gettext_strings.rs:24
msgid ""
"We will send you an email with a link you can use to remove any of your "
"email addresses from search."
msgstr "내 전자 메일 주소를 검색에서 지울 수 있는 링크가 담긴 전자 메일 한 통이 갈 거예요."
msgstr ""
"내 전자 메일 주소를 검색에서 지울 수 있는 링크가 담긴 전자 메일 한 통이 갈 거"
"예요."
#: src/gettext_strings.rs:25
msgid ""
"Managing the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
msgstr ""
"<span class=\"fingerprint\"><a href=\"{{ key_link }}\" target=\"_blank\">{{ "
"key_fpr }}</a></span> 키를 관리합니다."
"<span class=\"fingerprint\"><a href=\"{{ key_link }}\" target=\"_blank"
"\">{{ key_fpr }}</a></span> 키를 관리합니다."
#: src/gettext_strings.rs:26
msgid "Your key is published with the following identity information:"
msgstr "내 키에 대해 현재 아래 명의가 함께 공개돼 있습니다:"
#: src/gettext_strings.rs:27
msgid "Delete"
msgstr "지우기"
#: src/gettext_strings.rs:28
msgid ""
"Clicking \"delete\" on any address will remove it from this key. It will no "
"longer appear in a search.<br /> To add another address, <a "
"href=\"/upload\">upload</a> the key again."
"longer appear in a search.<br /> To add another address, <a href=\"/upload"
"\">upload</a> the key again."
msgstr ""
"\"지우기\"를 누르면 해당 주소가 내 키에서 지워지고 검색에 나타나지 않습니다.<br />다른 주소를 추가하려면 이 키를 다시 <a "
"href=\"/upload\">올리세요</a>."
"\"지우기\"를 누르면 해당 주소가 내 키에서 지워지고 검색에 나타나지 않습니다."
"<br />다른 주소를 추가하려면 이 키를 다시 <a href=\"/upload\">올리세요</a>."
#: src/gettext_strings.rs:29
msgid ""
"Your key is published as only non-identity information. (<a href=\"/about\""
" target=\"_blank\">What does this mean?</a>)"
"Your key is published as only non-identity information. (<a href=\"/about\" "
"target=\"_blank\">What does this mean?</a>)"
msgstr ""
"내 키의 비-명의 정보만 공개된 상태입니다. (<a href=\"/about\" target=\"_blank\">이게 뭘 "
"뜻하나요?</a>)"
"내 키의 비-명의 정보만 공개된 상태입니다. (<a href=\"/about\" target=\"_blank"
"\">이게 뭘 뜻하나요?</a>)"
#: src/gettext_strings.rs:30
msgid "To add an address, <a href=\"/upload\">upload</a> the key again."
msgstr "새 주소를 추가하려면 이 키를 다시 <a href=\"/upload\">올리세요</a>."
#: src/gettext_strings.rs:31
msgid ""
"We have sent an email with further instructions to <span class=\"email\">{{ "
"address }}</span>."
msgstr "상세 사용 설명이 담긴 전자 메일을 <span class=\"email\">{{ address }}</span> 주소로 보냈습니다."
"We have sent an email with further instructions to <span class=\"email"
"\">{{ address }}</span>."
msgstr ""
"상세 사용 설명이 담긴 전자 메일을 <span class=\"email\">{{ address }}</span> "
"주소로 보냈습니다."
#: src/gettext_strings.rs:32
msgid "This address has already been verified."
msgstr "이 주소는 이미 인증됐습니다."
#: src/gettext_strings.rs:33
msgid ""
"Your key <span class=\"fingerprint\">{{ key_fpr }}</span> is now published "
"for the identity <a href=\"{{userid_link}}\" target=\"_blank\"><span "
"class=\"email\">{{ userid }}</span></a>."
"for the identity <a href=\"{{userid_link}}\" target=\"_blank\"><span class="
"\"email\">{{ userid }}</span></a>."
msgstr ""
"이제 <a href=\"{{userid_link}}\" target=\"_blank\"><span class=\"email\">{{ "
"userid }}</span></a> 명의가 내 <span class=\"fingerprint\">{{ key_fpr }}</span> "
"키의 일부로 공개됩니다."
"이제 <a href=\"{{userid_link}}\" target=\"_blank\"><span class=\"email"
"\">{{ userid }}</span></a> 명의가 내 <span class=\"fingerprint"
"\">{{ key_fpr }}</span> 키의 일부로 공개됩니다."
#: src/gettext_strings.rs:34
msgid "Upload your key"
msgstr "내 키 올리기"
#: src/gettext_strings.rs:35
msgid "Upload"
msgstr "올리기"
#: src/gettext_strings.rs:36
msgid ""
"Need more info? Check our <a target=\"_blank\" href=\"/about\">intro</a> and"
" <a target=\"_blank\" href=\"/about/usage\">usage guide</a>."
"Need more info? Check our <a target=\"_blank\" href=\"/about\">intro</a> and "
"<a target=\"_blank\" href=\"/about/usage\">usage guide</a>."
msgstr ""
"도움이 더 필요한가요? <a target=\"_blank\" href=\"/about\">서비스 소개</a>와 <a "
"target=\"_blank\" href=\"/about/usage\">사용 안내서</a>를 읽어보세요."
"도움이 더 필요한가요? <a target=\"_blank\" href=\"/about\">서비스 소개</a>와 "
"<a target=\"_blank\" href=\"/about/usage\">사용 안내서</a>를 읽어보세요."
#: src/gettext_strings.rs:37
msgid ""
"You uploaded the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
msgstr ""
"<span class=\"fingerprint\"><a href=\"{{ key_link }}\" target=\"_blank\">{{ "
"key_fpr }}</a></span> 키를 올렸습니다."
"<span class=\"fingerprint\"><a href=\"{{ key_link }}\" target=\"_blank"
"\">{{ key_fpr }}</a></span> 키를 올렸습니다."
#: src/gettext_strings.rs:38
msgid "This key is revoked."
msgstr "이 키는 폐기됐습니다."
#: src/gettext_strings.rs:39
msgid ""
"It is published without identity information and can't be made available for"
" search by email address (<a href=\"/about\" target=\"_blank\">what does "
"this mean?</a>)."
"It is published without identity information and can't be made available for "
"search by email address (<a href=\"/about\" target=\"_blank\">what does this "
"mean?</a>)."
msgstr ""
"명의 정보가 없는 상태로 공개됐기 때문에 이 키는 전자 메일 주소로 찾을 수 없습니다. (<a href=\"/about\" "
"target=\"_blank\">이게 뭘 뜻하나요?</a>)"
"명의 정보가 없는 상태로 공개됐기 때문에 이 키는 전자 메일 주소로 찾을 수 없습"
"니다. (<a href=\"/about\" target=\"_blank\">이게 뭘 뜻하나요?</a>)"
#: src/gettext_strings.rs:40
msgid ""
"This key is now published with the following identity information (<a "
"href=\"/about\" target=\"_blank\">what does this mean?</a>):"
"This key is now published with the following identity information (<a href="
"\"/about\" target=\"_blank\">what does this mean?</a>):"
msgstr ""
"이제 다음 명의가 이 키의 일부로 함께 공개됩니다: (<a href=\"/about\" target=\"_blank\">이게 뭘 "
"뜻하나요?</a>)"
"이제 다음 명의가 이 키의 일부로 함께 공개됩니다: (<a href=\"/about\" target="
"\"_blank\">이게 뭘 뜻하나요?</a>)"
#: src/gettext_strings.rs:41
msgid "Published"
msgstr "공개됨"
#: src/gettext_strings.rs:42
msgid ""
"This key is now published with only non-identity information. (<a "
"href=\"/about\" target=\"_blank\">What does this mean?</a>)"
msgstr "이 키는 이제 비-명의 정보만 공개됩니다. (<a href=\"/about\" target=\"_blank\">이게 뭘 뜻하나요?</a>)"
"This key is now published with only non-identity information. (<a href=\"/"
"about\" target=\"_blank\">What does this mean?</a>)"
msgstr ""
"이 키는 이제 비-명의 정보만 공개됩니다. (<a href=\"/about\" target=\"_blank"
"\">이게 뭘 뜻하나요?</a>)"
#: src/gettext_strings.rs:43
msgid ""
"To make the key available for search by email address, you can verify it "
"belongs to you:"
msgstr "이 키를 전자 메일 주소로 찾을 수 있게 하려면 내가 진짜 이 주소를 가지고 있음을 인증하세요:"
msgstr ""
"이 키를 전자 메일 주소로 찾을 수 있게 하려면 내가 진짜 이 주소를 가지고 있음"
"을 인증하세요:"
#: src/gettext_strings.rs:44
msgid "Verification Pending"
msgstr "인증 대기 중"
#: src/gettext_strings.rs:45
msgid ""
"<strong>Note:</strong> Some providers delay emails for up to 15 minutes to "
"prevent spam. Please be patient."
msgstr ""
"<strong>알아두기:</strong> 몇몇 서비스 제공자는 스팸 방지를 위해 최대 15분까지 전자 메일 발송을 미루기도 합니다. "
"조금만 여유로이 기다려볼까요?"
"<strong>알아두기:</strong> 몇몇 서비스 제공자는 스팸 방지를 위해 최대 15분까"
"지 전자 메일 발송을 미루기도 합니다. 조금만 여유로이 기다려볼까요?"
#: src/gettext_strings.rs:46
msgid "Send Verification Email"
msgstr "인증 전자 메일 보내기"
#: src/gettext_strings.rs:47
msgid ""
"This key contains one identity that could not be parsed as an email "
"address.<br /> This identity can't be published on <span "
"class=\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-email-"
"uids\" target=\"_blank\">Why?</a>)"
"This key contains one identity that could not be parsed as an email address."
"<br /> This identity can't be published on <span class=\"brand\">keys."
"openpgp.org</span>. (<a href=\"/about/faq#non-email-uids\" target=\"_blank"
"\">Why?</a>)"
msgstr ""
"이 키에는 전자 메일 주소가 아닌 것으로 보이는 명의 하나가 포함돼 있네요.<br />이런 명의는 <span "
"class=\"brand\">keys.openpgp.org</span>에 공개할 수 없습니다. (<a href=\"/about/faq"
"#non-email-uids\" target=\"_blank\">왜죠?</a>)"
"이 키에는 전자 메일 주소가 아닌 것으로 보이는 명의 하나가 포함돼 있네요.<br /"
">이런 명의는 <span class=\"brand\">keys.openpgp.org</span>에 공개할 수 없습니"
"다. (<a href=\"/about/faq#non-email-uids\" target=\"_blank\">왜죠?</a>)"
#: src/gettext_strings.rs:48
msgid ""
"This key contains {{ count_unparsed }} identities that could not be parsed "
"as an email address.<br /> These identities can't be published on <span "
"class=\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-email-"
"uids\" target=\"_blank\">Why?</a>)"
msgstr ""
"이 키에는 전자 메일 주소가 아닌 것으로 보이는 명의 {{ count_unparsed }} 개가 포함돼 있네요.<br />이런 명의는 "
"<span class=\"brand\">keys.openpgp.org</span>에 공개할 수 없습니다. (<a "
"href=\"/about/faq#non-email-uids\" target=\"_blank\">왜죠?</a>)"
"이 키에는 전자 메일 주소가 아닌 것으로 보이는 명의 {{ count_unparsed }} 개가 "
"포함돼 있네요.<br />이런 명의는 <span class=\"brand\">keys.openpgp.org</span>"
"에 공개할 수 없습니다. (<a href=\"/about/faq#non-email-uids\" target=\"_blank"
"\">왜죠?</a>)"
#: src/gettext_strings.rs:49
msgid ""
"This key contains one revoked identity, which is not published. (<a "
"href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
"This key contains one revoked identity, which is not published. (<a href=\"/"
"about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
msgstr ""
"이 키에는 폐기된 명의 하나가 포함돼 있네요. 폐기된 명의는 공개하지 않습니다. (<a href=\"/about/faq#revoked-"
"uids\" target=\"_blank\">왜죠?</a>)"
"이 키에는 폐기된 명의 하나가 포함돼 있네요. 폐기된 명의는 공개하지 않습니다. "
"(<a href=\"/about/faq#revoked-uids\" target=\"_blank\">왜죠?</a>)"
#: src/gettext_strings.rs:50
msgid ""
"This key contains {{ count_revoked }} revoked identities, which are not "
"published. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
msgstr ""
"이 키에는 폐기된 명의 {{ count_revoked }} 개가 포함돼 있네요. 폐기된 명의는 공개하지 않습니다. (<a "
"href=\"/about/faq#revoked-uids\" target=\"_blank\">왜죠?</a>)"
"이 키에는 폐기된 명의 {{ count_revoked }} 개가 포함돼 있네요. 폐기된 명의는 "
"공개하지 않습니다. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">왜"
"죠?</a>)"
#: src/gettext_strings.rs:51
msgid "Your keys have been successfully uploaded:"
msgstr "내 키를 성공적으로 올렸습니다:"
#: src/gettext_strings.rs:52
msgid ""
"<strong>Note:</strong> To make keys searchable by email address, you must "
"upload them individually."
msgstr "<strong>알아두기:</strong> 개별 키를 전자 메일 주소로 찾을 수 있게 하려면 각각 따로 올리세요."
msgstr ""
"<strong>알아두기:</strong> 개별 키를 전자 메일 주소로 찾을 수 있게 하려면 각"
"각 따로 올리세요."
#: src/gettext_strings.rs:53
msgid "Verifying your email address…"
msgstr "전자 메일 주소를 인증하는 중..."
#: src/gettext_strings.rs:54
msgid ""
"If the process doesn't complete after a few seconds, please <input "
"type=\"submit\" class=\"textbutton\" value=\"click here\" />."
"If the process doesn't complete after a few seconds, please <input type="
"\"submit\" class=\"textbutton\" value=\"click here\" />."
msgstr ""
"몇 초가 지나도 이 절차가 끝나지 않는다면 <input type=\"submit\" class=\"textbutton\" "
"value=\"여기를 누르세요\" />."
"몇 초가 지나도 이 절차가 끝나지 않는다면 <input type=\"submit\" class="
"\"textbutton\" value=\"여기를 누르세요\" />."
#: src/gettext_strings.rs:56
msgid "Manage your key on {{domain}}"
msgstr "{{domain}}에 올린 내 키 관리"
#: src/gettext_strings.rs:58
msgid "Hi,"
msgstr "안녕하세요."
#: src/gettext_strings.rs:59
msgid ""
"This is an automated message from <a href=\"{{base_uri}}\" style=\"text-"
"decoration:none; color: #333\">{{domain}}</a>."
@@ -360,57 +289,48 @@ msgstr ""
"이 메시지는 <a href=\"{{base_uri}}\" style=\"text-decoration:none; color: "
"#333\">{{domain}}</a>에서 자동으로 발송됐습니다."
#: src/gettext_strings.rs:60
msgid "If you didn't request this message, please ignore it."
msgstr "만약 직접 요청하신 게 아니라면 그냥 무시하세요."
#: src/gettext_strings.rs:61
msgid "OpenPGP key: <tt>{{primary_fp}}</tt>"
msgstr "OpenPGP 키: <tt>{{primary_fp}}</tt>"
#: src/gettext_strings.rs:62
msgid ""
"To manage and delete listed addresses on this key, please follow the link "
"below:"
msgstr "위 키에 엮인 주소를 숨기는 등의 키 관리는 아래 링크에서 할 수 있어요:"
#: src/gettext_strings.rs:63
msgid ""
"You can find more info at <a "
"href=\"{{base_uri}}/about\">{{domain}}/about</a>."
msgstr "더 자세한 사항은 <a href=\"{{base_uri}}/about\">{{domain}}/about</a> 페이지를 참고하세요."
"You can find more info at <a href=\"{{base_uri}}/about\">{{domain}}/about</"
"a>."
msgstr ""
"더 자세한 사항은 <a href=\"{{base_uri}}/about\">{{domain}}/about</a> 페이지"
"를 참고하세요."
#: src/gettext_strings.rs:64
msgid "distributing OpenPGP keys since 2019"
msgstr "2019년 이래 OpenPGP 키 배포에 이바지하는 중"
#: src/gettext_strings.rs:67
msgid "This is an automated message from {{domain}}."
msgstr "이 메시지는 {{domain}}에서 자동으로 발송됐습니다."
#: src/gettext_strings.rs:69
msgid "OpenPGP key: {{primary_fp}}"
msgstr "OpenPGP 키: {{primary_fp}}"
#: src/gettext_strings.rs:71
msgid "You can find more info at {{base_uri}}/about"
msgstr "더 자세한 사항은 {{base_uri}}/about 페이지를 참고하세요."
#: src/gettext_strings.rs:74
msgid "Verify {{userid}} for your key on {{domain}}"
msgstr "{{domain}}에 {{userid}} 명의로 올린 키 인증"
#: src/gettext_strings.rs:80
msgid ""
"To let others find this key from your email address \"<a rel=\"nofollow\" "
"href=\"#\" style=\"text-decoration:none; color: #333\">{{userid}}</a>\", "
"please click the link below:"
msgstr ""
"다른 사람들이 내 전자 메일 주소(<a rel=\"nofollow\" href=\"#\" style=\"text-"
"decoration:none; color: #333\">{{userid}}</a>)로 위 키를 찾을 수 있게 하려면 아래 링크를 "
"따라가세요:"
"decoration:none; color: #333\">{{userid}}</a>)로 위 키를 찾을 수 있게 하려면 "
"아래 링크를 따라가세요:"
#: src/gettext_strings.rs:88
msgid ""
"To let others find this key from your email address \"{{userid}}\",\n"
"please follow the link below:"
@@ -418,42 +338,57 @@ msgstr ""
"다른 사람들이 내 전자 메일 주소({{userid}})로 위 키를 찾을 수 있게 하려면\n"
"아래 링크를 따라가세요:"
#: src/web/manage.rs:103
msgid "No key found for fingerprint {}"
msgstr "지문 {}에 해당하는 키를 찾지 못했습니다."
msgid "No key found for key id {}"
msgstr "키 ID {}에 해당하는 키를 찾지 못했습니다."
msgid "No key found for email address {}"
msgstr "전자 메일 주소 {}에 해당하는 키를 찾지 못했습니다."
msgid "Search by Short Key ID is not supported."
msgstr "'짧은 키 ID' 형식은 지원하지 않습니다."
msgid "Invalid search query."
msgstr "올바르지 않은 검색어입니다."
msgctxt "Subject for verification email, {0} = userid, {1} = keyserver domain"
msgid "Verify {0} for your key on {1}"
msgstr "{1}에 {0} 명의로 올린 키 인증"
msgctxt "Subject for manage email, {} = keyserver domain"
msgid "Manage your key on {}"
msgstr "{}에 올린 내 키 관리"
msgid "This link is invalid or expired"
msgstr "링크가 올바르지 않거나 이미 만료됐습니다"
#: src/web/manage.rs:129
msgid "Malformed address: {address}"
msgstr "잘못된 형식의 주소: {address}"
#, fuzzy
msgid "Malformed address: {}"
msgstr "잘못된 형식의 주소: {}"
#: src/web/manage.rs:136
msgid "No key for address: {address}"
msgstr "다음 주소와 엮인 키 없음: {address}"
#, fuzzy
msgid "No key for address: {}"
msgstr "다음 주소와 엮인 키 없음: {}"
#: src/web/manage.rs:152
msgid "A request has already been sent for this address recently."
msgstr "최근에 이 주소로 이미 요청이 접수됐습니다."
#: src/web/vks.rs:111
msgid "Parsing of key data failed."
msgstr "키 데이터를 해석하지 못했습니다."
#: src/web/vks.rs:120
msgid "Whoops, please don't upload secret keys!"
msgstr "아이고 맙소사! 비밀키는 올리면 안 돼요!"
#: src/web/vks.rs:133
msgid "No key uploaded."
msgstr "아무 키도 올리지 않았습니다."
#: src/web/vks.rs:177
msgid "Error processing uploaded key."
msgstr "올린 키를 처리하지 못했습니다."
#: src/web/vks.rs:247
msgid "Upload session expired. Please try again."
msgstr "올리기 세션이 만료됐습니다. 다시 해보세요."
#: src/web/vks.rs:284
msgid "Invalid verification link."
msgstr "올바르지 않은 인증 링크"

View File

@@ -1,5 +1,7 @@
#
# Translators:
# Kristoffer Håvik, 2020
# Vincent Breitmoser <look@my.amazin.horse>, 2020
# Kristoffer Håvik, 2021
#
msgid ""
msgstr ""
@@ -7,7 +9,7 @@ msgstr ""
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2019-09-27 18:05+0000\n"
"Last-Translator: Kristoffer Håvik, 2020\n"
"Last-Translator: Kristoffer Håvik, 2021\n"
"Language-Team: Norwegian Bokmål (https://www.transifex.com/otf/teams/102430/"
"nb/)\n"
"Language: nb\n"
@@ -16,58 +18,21 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: src/mail.rs:107
msgctxt "Subject for verification email"
msgid "Verify {userid} for your key on {domain}"
msgstr "Bekreft {userid} for nøkkelen din på {domain}"
#: src/mail.rs:140
msgctxt "Subject for manage email"
msgid "Manage your key on {domain}"
msgstr "Administrer nøkkelen din på {domain}"
#: src/i18n_helpers.rs:8
msgid "No key found for fingerprint {fingerprint}"
msgstr ""
#: src/i18n_helpers.rs:10
msgid "No key found for key id {key_id}"
msgstr ""
#: src/i18n_helpers.rs:12
#, fuzzy
msgid "No key found for email address {email}"
msgstr "Ingen nøkler for adressen {address}"
#: src/i18n_helpers.rs:13
msgid "Search by Short Key ID is not supported."
msgstr ""
#: src/i18n_helpers.rs:14
msgid "Invalid search query."
msgstr ""
#: src/gettext_strings.rs:4
msgid "Error"
msgstr "Feil"
#: src/gettext_strings.rs:5
msgid "Looks like something went wrong :("
msgstr "Det ser ut til at noe gikk galt. :("
#: src/gettext_strings.rs:6
msgid "Error message: {{ internal_error }}"
msgstr "Feilmelding: {{ internal_error }}"
#: src/gettext_strings.rs:7
msgid "There was an error with your request:"
msgstr "Det skjedde en feil ved behandling av forespørselen din:"
#: src/gettext_strings.rs:8
msgid "We found an entry for <span class=\"email\">{{ query }}</span>:"
msgstr "Vi fant en oppføring for <span class=\"email\">{{ query }}</span>:"
#: src/gettext_strings.rs:9
msgid ""
"<strong>Hint:</strong> It's more convenient to use <span class=\"brand"
"\">keys.openpgp.org</span> from your OpenPGP software.<br /> Take a look at "
@@ -77,19 +42,15 @@ msgstr ""
"\">keys.openpgp.org</span> via OpenPGP-programvaren.<br /> Les gjerne <a "
"href=\"/about/usage\">brukerveiledningen</a> for detaljer."
#: src/gettext_strings.rs:10
msgid "debug info"
msgstr "feilsøkingsinfo"
#: src/gettext_strings.rs:11
msgid "Search by Email Address / Key ID / Fingerprint"
msgstr "Søk etter e-postadresse / nøkkel-ID / fingeravtrykk"
#: src/gettext_strings.rs:12
msgid "Search"
msgstr "Søk"
#: src/gettext_strings.rs:13
msgid ""
"You can also <a href=\"/upload\">upload</a> or <a href=\"/manage\">manage</"
"a> your key."
@@ -97,29 +58,25 @@ msgstr ""
"Du kan også <a href=\"/upload\">laste opp</a> eller <a href=\"/manage"
"\">administrere</a> nøkkelen din."
#: src/gettext_strings.rs:14
msgid "Find out more <a href=\"/about\">about this service</a>."
msgstr "Finn ut mer <a href=\"/about\">om denne tjenesten</a>."
#: src/gettext_strings.rs:15
msgid "News:"
msgstr "Nyheter:"
#: src/gettext_strings.rs:16
msgid ""
"<a href=\"/about/news#2019-11-12-celebrating-100k\">Celebrating 100.000 "
"verified addresses! 📈</a> (2019-11-12)"
msgstr ""
"<a href=\"/about/news#2019-11-12-celebrating-100k\">Vi feirer 100 000 "
"bekreftede adresser! 📈</a> (2019-11-12)"
#: src/gettext_strings.rs:17
msgid "v{{ version }} built from"
msgstr "v{{ version }} bygd fra"
#: src/gettext_strings.rs:18
msgid "Powered by <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>"
msgstr "Drevet av <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>"
#: src/gettext_strings.rs:19
msgid ""
"Background image retrieved from <a href=\"https://www.toptal.com/designers/"
"subtlepatterns/subtle-grey/\">Subtle Patterns</a> under CC BY-SA 3.0"
@@ -127,23 +84,18 @@ msgstr ""
"Bakgrunnsbildet er hentet fra <a href=\"https://www.toptal.com/designers/"
"subtlepatterns/subtle-grey/\">Subtle Patterns</a> på lisens CC BY-SA 3.0"
#: src/gettext_strings.rs:20
msgid "Maintenance Mode"
msgstr "Vedlikeholdsmodus"
#: src/gettext_strings.rs:21
msgid "Manage your key"
msgstr "Administrer nøkkelen din"
#: src/gettext_strings.rs:22
msgid "Enter any verified email address for your key"
msgstr "Skriv inn en bekreftet e-postadresse som tilhører nøkkelen din"
#: src/gettext_strings.rs:23
msgid "Send link"
msgstr "Send lenke"
#: src/gettext_strings.rs:24
msgid ""
"We will send you an email with a link you can use to remove any of your "
"email addresses from search."
@@ -151,7 +103,6 @@ msgstr ""
"Vi sender deg en e-post med en lenke som du kan bruke for å ta bort e-"
"postadressene dine fra søket."
#: src/gettext_strings.rs:25
msgid ""
"Managing the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
@@ -159,15 +110,12 @@ msgstr ""
"Administrerer nøkkelen <span class=\"fingerprint\"><a href="
"\"{{ key_link }}\" target=\"_blank\">{{ key_fpr }}</a></span>."
#: src/gettext_strings.rs:26
msgid "Your key is published with the following identity information:"
msgstr "Nøkkelen din er offentliggjort med følgende identitetsinformasjon:"
#: src/gettext_strings.rs:27
msgid "Delete"
msgstr "Slett"
#: src/gettext_strings.rs:28
msgid ""
"Clicking \"delete\" on any address will remove it from this key. It will no "
"longer appear in a search.<br /> To add another address, <a href=\"/upload"
@@ -177,7 +125,6 @@ msgstr ""
"nøkkelen og vil ikke lenger dukke opp i søk.<br /> For å legge til en annen "
"adresse, <a href=\"/upload\">last opp</a> nøkkelen på nytt."
#: src/gettext_strings.rs:29
msgid ""
"Your key is published as only non-identity information. (<a href=\"/about\" "
"target=\"_blank\">What does this mean?</a>)"
@@ -185,13 +132,11 @@ msgstr ""
"Nøkkelen din er offentliggjort uten identitetsinformasjon. (<a href=\"/about"
"\" target=\"_blank\">Hva betyr dette?</a>)"
#: src/gettext_strings.rs:30
msgid "To add an address, <a href=\"/upload\">upload</a> the key again."
msgstr ""
"For å legge til en adresse, <a href=\"/upload\">last opp</a> nøkkelen på "
"nytt."
#: src/gettext_strings.rs:31
msgid ""
"We have sent an email with further instructions to <span class=\"email"
"\">{{ address }}</span>."
@@ -199,11 +144,9 @@ msgstr ""
"Vi har sendt en e-post med videre instruksjoner til <span class=\"email"
"\">{{ address }}</span>."
#: src/gettext_strings.rs:32
msgid "This address has already been verified."
msgstr "Denne adressen er allerede bekreftet."
#: src/gettext_strings.rs:33
msgid ""
"Your key <span class=\"fingerprint\">{{ key_fpr }}</span> is now published "
"for the identity <a href=\"{{userid_link}}\" target=\"_blank\"><span class="
@@ -213,15 +156,12 @@ msgstr ""
"offentliggjort for identiteten <a href=\"{{userid_link}}\" target=\"_blank"
"\"><span class=\"email\">{{ userid }}</span></a>."
#: src/gettext_strings.rs:34
msgid "Upload your key"
msgstr "Last opp nøkkelen din"
#: src/gettext_strings.rs:35
msgid "Upload"
msgstr "Last opp"
#: src/gettext_strings.rs:36
msgid ""
"Need more info? Check our <a target=\"_blank\" href=\"/about\">intro</a> and "
"<a target=\"_blank\" href=\"/about/usage\">usage guide</a>."
@@ -229,7 +169,6 @@ msgstr ""
"Vil du vite mer? Les <a target=\"_blank\" href=\"/about\">innføringen</a> og "
"<a target=\"_blank\" href=\"/about/usage\">bruksanvisningen</a>."
#: src/gettext_strings.rs:37
msgid ""
"You uploaded the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
@@ -237,11 +176,9 @@ msgstr ""
"Du har lastet opp nøkkelen <span class=\"fingerprint\"><a href="
"\"{{ key_link }}\" target=\"_blank\">{{ key_fpr}}</a></span>."
#: src/gettext_strings.rs:38
msgid "This key is revoked."
msgstr "Denne nøkkelen har blitt trukket tilbake."
#: src/gettext_strings.rs:39
msgid ""
"It is published without identity information and can't be made available for "
"search by email address (<a href=\"/about\" target=\"_blank\">what does this "
@@ -251,7 +188,6 @@ msgstr ""
"tilgjengelig i e-postadressesøk (<a href=\"/about\" target=\"_blank\">hva "
"betyr dette?</a>)."
#: src/gettext_strings.rs:40
msgid ""
"This key is now published with the following identity information (<a href="
"\"/about\" target=\"_blank\">what does this mean?</a>):"
@@ -259,11 +195,9 @@ msgstr ""
"Denne nøkkelen er nå offentliggjort med følgende identitetsinformasjon (<a "
"href=\"/about\" target=\"_blank\">hva betyr dette?</a>):"
#: src/gettext_strings.rs:41
msgid "Published"
msgstr "Offentliggjort"
#: src/gettext_strings.rs:42
msgid ""
"This key is now published with only non-identity information. (<a href=\"/"
"about\" target=\"_blank\">What does this mean?</a>)"
@@ -271,7 +205,6 @@ msgstr ""
"Denne nøkkelen er nå offentliggjort uten identitetsinformasjon. (<a href=\"/"
"about\" target=\"_blank\">Hva betyr dette?</a>)"
#: src/gettext_strings.rs:43
msgid ""
"To make the key available for search by email address, you can verify it "
"belongs to you:"
@@ -279,11 +212,9 @@ msgstr ""
"For at nøkkelen skal kunne finnes ved å søke etter e-postadresse, kan du "
"bekrefte at den tilhører deg:"
#: src/gettext_strings.rs:44
msgid "Verification Pending"
msgstr "Bekreftelse pågår"
#: src/gettext_strings.rs:45
msgid ""
"<strong>Note:</strong> Some providers delay emails for up to 15 minutes to "
"prevent spam. Please be patient."
@@ -291,11 +222,9 @@ msgstr ""
"<strong>Merk:</strong> Noen leverandører forsinker e-poster med inntil 15 "
"min. for å forhindre søppelpost. Vennligst vent."
#: src/gettext_strings.rs:46
msgid "Send Verification Email"
msgstr "Send bekreftelses-epost"
#: src/gettext_strings.rs:47
msgid ""
"This key contains one identity that could not be parsed as an email address."
"<br /> This identity can't be published on <span class=\"brand\">keys."
@@ -307,7 +236,6 @@ msgstr ""
"\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-email-uids\" "
"target=\"_blank\">Hvorfor?</a>)"
#: src/gettext_strings.rs:48
msgid ""
"This key contains {{ count_unparsed }} identities that could not be parsed "
"as an email address.<br /> These identities can't be published on <span "
@@ -319,7 +247,6 @@ msgstr ""
"på <span class=\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-"
"email-uids\" target=\"_blank\">Hvorfor?</a>)"
#: src/gettext_strings.rs:49
msgid ""
"This key contains one revoked identity, which is not published. (<a href=\"/"
"about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
@@ -328,7 +255,6 @@ msgstr ""
"ikke er offentliggjort. (<a href=\"/about/faq#revoked-uids\" target=\"_blank"
"\">Hvorfor?</a>)"
#: src/gettext_strings.rs:50
msgid ""
"This key contains {{ count_revoked }} revoked identities, which are not "
"published. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
@@ -337,11 +263,9 @@ msgstr ""
"trukket tilbake og som ikke er offentliggjorte. (<a href=\"/about/"
"faq#revoked-uids\" target=\"_blank\">Hvorfor?</a>)"
#: src/gettext_strings.rs:51
msgid "Your keys have been successfully uploaded:"
msgstr "Nøklene dine er lastet opp:"
#: src/gettext_strings.rs:52
msgid ""
"<strong>Note:</strong> To make keys searchable by email address, you must "
"upload them individually."
@@ -349,11 +273,9 @@ msgstr ""
"<strong>Merk:</strong> For at nøkler skal kunne finnes ved å søke etter e-"
"postadresse, må de lastes opp én og én."
#: src/gettext_strings.rs:53
msgid "Verifying your email address…"
msgstr "Bekrefter e-postadressen din…"
#: src/gettext_strings.rs:54
msgid ""
"If the process doesn't complete after a few seconds, please <input type="
"\"submit\" class=\"textbutton\" value=\"click here\" />."
@@ -361,15 +283,12 @@ msgstr ""
"Dersom det tar mer enn noen få sekunder, <input type=\"submit\" class="
"\"textbutton\" value=\"klikk her\"/>."
#: src/gettext_strings.rs:56
msgid "Manage your key on {{domain}}"
msgstr "Administrer nøkkelen din på {{domain}}"
#: src/gettext_strings.rs:58
msgid "Hi,"
msgstr "Hei,"
#: src/gettext_strings.rs:59
msgid ""
"This is an automated message from <a href=\"{{base_uri}}\" style=\"text-"
"decoration:none; color: #333\">{{domain}}</a>."
@@ -377,16 +296,13 @@ msgstr ""
"Dette er en automatisk beskjed fra <a href=\"{{base_uri}}\" style=\"text-"
"decoration:none; color: #333\">{{domain}}</a>."
#: src/gettext_strings.rs:60
msgid "If you didn't request this message, please ignore it."
msgstr ""
"Hvis ikke det var du som ba om denne beskjeden, kan du se bort fra den."
#: src/gettext_strings.rs:61
msgid "OpenPGP key: <tt>{{primary_fp}}</tt>"
msgstr "OpenPGP-nøkkel: <tt>{{primary_fp}}</tt>"
#: src/gettext_strings.rs:62
msgid ""
"To manage and delete listed addresses on this key, please follow the link "
"below:"
@@ -394,7 +310,6 @@ msgstr ""
"For å administrere og slette adressene som hører til denne nøkkelen, "
"vennligst følg lenken nedenfor:"
#: src/gettext_strings.rs:63
msgid ""
"You can find more info at <a href=\"{{base_uri}}/about\">{{domain}}/about</"
"a>."
@@ -402,27 +317,21 @@ msgstr ""
"Du finner mer informasjon på <a href=\"{{base_uri}}/about\">{{domain}}/"
"about</a>."
#: src/gettext_strings.rs:64
msgid "distributing OpenPGP keys since 2019"
msgstr "har distribuert OpenPGP-nøkler siden 2019"
#: src/gettext_strings.rs:67
msgid "This is an automated message from {{domain}}."
msgstr "Dette er en automatisk melding fra {{domain}}."
#: src/gettext_strings.rs:69
msgid "OpenPGP key: {{primary_fp}}"
msgstr "OpenPGP-nøkkel: {{primary_fp}}"
#: src/gettext_strings.rs:71
msgid "You can find more info at {{base_uri}}/about"
msgstr "Du finner mer informasjon på {{base_uri}}/about"
#: src/gettext_strings.rs:74
msgid "Verify {{userid}} for your key on {{domain}}"
msgstr "Bekreft {{userid}} for nøkkelen din på {{domain}}"
#: src/gettext_strings.rs:80
msgid ""
"To let others find this key from your email address \"<a rel=\"nofollow\" "
"href=\"#\" style=\"text-decoration:none; color: #333\">{{userid}}</a>\", "
@@ -433,7 +342,6 @@ msgstr ""
"#333\">{{userid}}</a>\",\n"
"vennligst klikk på lenken nedenfor:"
#: src/gettext_strings.rs:88
msgid ""
"To let others find this key from your email address \"{{userid}}\",\n"
"please follow the link below:"
@@ -442,49 +350,57 @@ msgstr ""
"\"{{userid}}\",\n"
"vennligst følg lenken nedenfor:"
#: src/web/manage.rs:103
msgid "No key found for fingerprint {}"
msgstr "Fant ingen nøkkel for fingeravtrykk {}"
msgid "No key found for key id {}"
msgstr "Fant ingen nøkkel for nøkkel-ID {}"
msgid "No key found for email address {}"
msgstr "Fant ingen nøkkel for e-postadresse {}"
msgid "Search by Short Key ID is not supported."
msgstr "Tjenesten støtter ikke søk etter forkortet nøkkel-ID."
msgid "Invalid search query."
msgstr "Ugyldig forespørsel."
msgctxt "Subject for verification email, {0} = userid, {1} = keyserver domain"
msgid "Verify {0} for your key on {1}"
msgstr "Bekreft {0} for nøkkelen din på {1}"
msgctxt "Subject for manage email, {} = keyserver domain"
msgid "Manage your key on {}"
msgstr "Administrer nøkkelen din på {}"
msgid "This link is invalid or expired"
msgstr "Denne lenken er ugyldig eller utløpt"
#: src/web/manage.rs:129
msgid "Malformed address: {address}"
msgstr "Deformert adresse: {address}"
#, fuzzy
msgid "Malformed address: {}"
msgstr "Deformert adresse: {}"
#: src/web/manage.rs:136
msgid "No key for address: {address}"
msgstr "Ingen nøkler for adressen {address}"
#, fuzzy
msgid "No key for address: {}"
msgstr "Ingen nøkler for adressen {}"
#: src/web/manage.rs:152
msgid "A request has already been sent for this address recently."
msgstr "En forespørsel har allerede blitt sendt for denne adressen nylig."
#: src/web/vks.rs:111
msgid "Parsing of key data failed."
msgstr "Kunne ikke analysere innholdet i nøkkelen."
#: src/web/vks.rs:120
msgid "Whoops, please don't upload secret keys!"
msgstr "Hopp sann! Vennligst ikke last opp hemmelige nøkler!"
#: src/web/vks.rs:133
msgid "No key uploaded."
msgstr "Ingen nøkkel er lastet opp."
#: src/web/vks.rs:177
msgid "Error processing uploaded key."
msgstr "Kunne ikke behandle den opplastede nøkkelen."
#: src/web/vks.rs:247
msgid "Upload session expired. Please try again."
msgstr "Opplastingsøkten er utgått. Vennligst prøv på nytt."
#: src/web/vks.rs:284
msgid "Invalid verification link."
msgstr "Bekreftelseslenken er ugyldig."
#~ msgid ""
#~ "<a href=\"/about/news#2019-09-12-three-months-later\">Three months after "
#~ "launch ✨</a> (2019-09-12)"
#~ msgstr ""
#~ "<a href=\"/about/news#2019-09-12-three-months-later\">Tre måneder etter "
#~ "lansering ✨</a> (12. september 2019)"

View File

@@ -16,58 +16,21 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: src/mail.rs:107
msgctxt "Subject for verification email"
msgid "Verify {userid} for your key on {domain}"
msgstr "Verifieer {userid} voor uw sleutel op {domain}"
#: src/mail.rs:140
msgctxt "Subject for manage email"
msgid "Manage your key on {domain}"
msgstr "Beheer uw sleutel op {domain}"
#: src/i18n_helpers.rs:8
msgid "No key found for fingerprint {fingerprint}"
msgstr ""
#: src/i18n_helpers.rs:10
msgid "No key found for key id {key_id}"
msgstr ""
#: src/i18n_helpers.rs:12
#, fuzzy
msgid "No key found for email address {email}"
msgstr "Geen sleutel voor adres: {address}"
#: src/i18n_helpers.rs:13
msgid "Search by Short Key ID is not supported."
msgstr ""
#: src/i18n_helpers.rs:14
msgid "Invalid search query."
msgstr ""
#: src/gettext_strings.rs:4
msgid "Error"
msgstr "Fout"
#: src/gettext_strings.rs:5
msgid "Looks like something went wrong :("
msgstr "Het lijkt erop dat er iets mis is gegaan :("
#: src/gettext_strings.rs:6
msgid "Error message: {{ internal_error }}"
msgstr "Foutmelding: {{internal_error}}"
#: src/gettext_strings.rs:7
msgid "There was an error with your request:"
msgstr "Er is een fout opgetreden bij uw verzoek:"
#: src/gettext_strings.rs:8
msgid "We found an entry for <span class=\"email\">{{ query }}</span>:"
msgstr "We hebben een vermelding gevonden voor {{query}}:"
#: src/gettext_strings.rs:9
msgid ""
"<strong>Hint:</strong> It's more convenient to use <span class=\"brand"
"\">keys.openpgp.org</span> from your OpenPGP software.<br /> Take a look at "
@@ -77,19 +40,15 @@ msgstr ""
"org</span> te gebruiken vanuit uw OpenPGP-software.<br /> Bekijk onze <a "
"href=\"/about/usage\">gebruikershandleiding</a> voor meer informatie."
#: src/gettext_strings.rs:10
msgid "debug info"
msgstr "debug info"
#: src/gettext_strings.rs:11
msgid "Search by Email Address / Key ID / Fingerprint"
msgstr "Zoeken op e-mailadres / sleutel-ID / vingerafdruk"
#: src/gettext_strings.rs:12
msgid "Search"
msgstr "Zoek"
#: src/gettext_strings.rs:13
msgid ""
"You can also <a href=\"/upload\">upload</a> or <a href=\"/manage\">manage</"
"a> your key."
@@ -97,29 +56,23 @@ msgstr ""
"U kunt uw sleutel ook <a href=\"/upload\">uploaden</a> of <a href=\"/manage"
"\">beheren</a>."
#: src/gettext_strings.rs:14
msgid "Find out more <a href=\"/about\">about this service</a>."
msgstr "Lees meer over <a href=\"/about\">deze dienst</a>."
#: src/gettext_strings.rs:15
msgid "News:"
msgstr "Nieuws:"
#: src/gettext_strings.rs:16
msgid ""
"<a href=\"/about/news#2019-11-12-celebrating-100k\">Celebrating 100.000 "
"verified addresses! 📈</a> (2019-11-12)"
msgstr ""
#: src/gettext_strings.rs:17
msgid "v{{ version }} built from"
msgstr "v{{version}} gebouwd van"
#: src/gettext_strings.rs:18
msgid "Powered by <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>"
msgstr "Powered by <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>"
#: src/gettext_strings.rs:19
msgid ""
"Background image retrieved from <a href=\"https://www.toptal.com/designers/"
"subtlepatterns/subtle-grey/\">Subtle Patterns</a> under CC BY-SA 3.0"
@@ -128,23 +81,18 @@ msgstr ""
"designers/subtlepatterns/subtle-grey/\">Subtle Patterns</a> onder CC BY-SA "
"3.0"
#: src/gettext_strings.rs:20
msgid "Maintenance Mode"
msgstr "Onderhoud"
#: src/gettext_strings.rs:21
msgid "Manage your key"
msgstr "Beheer uw sleutel"
#: src/gettext_strings.rs:22
msgid "Enter any verified email address for your key"
msgstr "Voer één van de geverifieerd e-mailadressen van uw sleutel in"
#: src/gettext_strings.rs:23
msgid "Send link"
msgstr "Stuur link"
#: src/gettext_strings.rs:24
msgid ""
"We will send you an email with a link you can use to remove any of your "
"email addresses from search."
@@ -152,7 +100,6 @@ msgstr ""
"We sturen u een e-mail met een link die u kunt gebruiken om al uw e-"
"mailadressen uit de zoekresultaten te verwijderen."
#: src/gettext_strings.rs:25
msgid ""
"Managing the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
@@ -160,15 +107,12 @@ msgstr ""
"Beheren van de sleutel <span class=\"fingerprint\"><a href="
"\"{{ key_link }}\" target=\"_blank\"> {{key_fpr}}</a></span>."
#: src/gettext_strings.rs:26
msgid "Your key is published with the following identity information:"
msgstr "Uw sleutel wordt gepubliceerd met de volgende identiteitsgegevens:"
#: src/gettext_strings.rs:27
msgid "Delete"
msgstr "Verwijderen"
#: src/gettext_strings.rs:28
msgid ""
"Clicking \"delete\" on any address will remove it from this key. It will no "
"longer appear in a search.<br /> To add another address, <a href=\"/upload"
@@ -179,7 +123,6 @@ msgstr ""
"<br /> <a href=\"/upload\">Upload</a> de sleutel opnieuw om een ander adres "
"toe te voegen."
#: src/gettext_strings.rs:29
msgid ""
"Your key is published as only non-identity information. (<a href=\"/about\" "
"target=\"_blank\">What does this mean?</a>)"
@@ -187,12 +130,10 @@ msgstr ""
"Uw sleutel wordt gepubliceerd als niet-identiteitsinformatie. (<a href=\"/"
"about\" target=\"_blank\">Wat betekent dit?</a>)"
#: src/gettext_strings.rs:30
msgid "To add an address, <a href=\"/upload\">upload</a> the key again."
msgstr ""
"<a href=\"/upload\">Upload</a> de sleutel opnieuw om een adres toe te voegen."
#: src/gettext_strings.rs:31
msgid ""
"We have sent an email with further instructions to <span class=\"email"
"\">{{ address }}</span>."
@@ -200,11 +141,9 @@ msgstr ""
"We hebben een e-mail gestuurd met verdere instructies naar <span class="
"\"email\">{{address}}</span>."
#: src/gettext_strings.rs:32
msgid "This address has already been verified."
msgstr "Dit adres is al geverifieerd."
#: src/gettext_strings.rs:33
msgid ""
"Your key <span class=\"fingerprint\">{{ key_fpr }}</span> is now published "
"for the identity <a href=\"{{userid_link}}\" target=\"_blank\"><span class="
@@ -214,15 +153,12 @@ msgstr ""
"voor de identiteit <a href=\"{{userid_link}}\" target=\"_blank\"><span class="
"\"email\">{{userid}}</span></a>."
#: src/gettext_strings.rs:34
msgid "Upload your key"
msgstr "Upload uw sleutel"
#: src/gettext_strings.rs:35
msgid "Upload"
msgstr "Upload"
#: src/gettext_strings.rs:36
msgid ""
"Need more info? Check our <a target=\"_blank\" href=\"/about\">intro</a> and "
"<a target=\"_blank\" href=\"/about/usage\">usage guide</a>."
@@ -231,7 +167,6 @@ msgstr ""
"\">intro</a> en <a target=\"_blank\" href=\"/about/usage"
"\">gebruikshandleiding</a>."
#: src/gettext_strings.rs:37
msgid ""
"You uploaded the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
@@ -239,11 +174,9 @@ msgstr ""
"U heeft de sleutel <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{key_fpr}}</a></span> geüpload."
#: src/gettext_strings.rs:38
msgid "This key is revoked."
msgstr "Deze sleutel is ingetrokken."
#: src/gettext_strings.rs:39
msgid ""
"It is published without identity information and can't be made available for "
"search by email address (<a href=\"/about\" target=\"_blank\">what does this "
@@ -253,7 +186,6 @@ msgstr ""
"doorzocht op e-mailadres (<a href=\"/about\" target=\"_blank\">wat betekent "
"dit?</a>)."
#: src/gettext_strings.rs:40
msgid ""
"This key is now published with the following identity information (<a href="
"\"/about\" target=\"_blank\">what does this mean?</a>):"
@@ -261,11 +193,9 @@ msgstr ""
"Deze sleutel wordt nu gepubliceerd met de volgende identiteitsgegevens (<a "
"href=\"/about\" target=\"_blank\">wat betekent dit?</a>):"
#: src/gettext_strings.rs:41
msgid "Published"
msgstr "Gepubliceerd"
#: src/gettext_strings.rs:42
msgid ""
"This key is now published with only non-identity information. (<a href=\"/"
"about\" target=\"_blank\">What does this mean?</a>)"
@@ -273,7 +203,6 @@ msgstr ""
"Deze sleutel wordt nu gepubliceerd zonder identiteitsinformatie. (<a href=\"/"
"about\" target=\"_blank\">Wat betekent dit?</a>)"
#: src/gettext_strings.rs:43
msgid ""
"To make the key available for search by email address, you can verify it "
"belongs to you:"
@@ -281,11 +210,9 @@ msgstr ""
"Om de sleutel beschikbaar te maken voor zoeken op e-mailadres, kunt u "
"valideren dat deze van u is:"
#: src/gettext_strings.rs:44
msgid "Verification Pending"
msgstr "Verificatie in behandeling"
#: src/gettext_strings.rs:45
msgid ""
"<strong>Note:</strong> Some providers delay emails for up to 15 minutes to "
"prevent spam. Please be patient."
@@ -293,11 +220,9 @@ msgstr ""
"<strong>Opmerking:</strong> sommige providers vertragen e-mails maximaal 15 "
"minuten om spam te voorkomen. Wees alstublieft geduldig."
#: src/gettext_strings.rs:46
msgid "Send Verification Email"
msgstr "Verzend verificatie-e-mail"
#: src/gettext_strings.rs:47
msgid ""
"This key contains one identity that could not be parsed as an email address."
"<br /> This identity can't be published on <span class=\"brand\">keys."
@@ -309,7 +234,6 @@ msgstr ""
"\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-email-uids\" "
"target=\"_blank\">Waarom?</a>)"
#: src/gettext_strings.rs:48
msgid ""
"This key contains {{ count_unparsed }} identities that could not be parsed "
"as an email address.<br /> These identities can't be published on <span "
@@ -321,7 +245,6 @@ msgstr ""
"gepubliceerd op <span class=\"brand\">keys.openpgp.org</span>. (<a href=\"/"
"about/faq#non-email-uids\" target=\"_blank\">Waarom?</a>)"
#: src/gettext_strings.rs:49
msgid ""
"This key contains one revoked identity, which is not published. (<a href=\"/"
"about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
@@ -329,7 +252,6 @@ msgstr ""
"Deze sleutel bevat één ingetrokken identiteit, die niet is gepubliceerd. (<a "
"href=\"/about/faq#revoked-uids\" target=\"_blank\">Waarom?</a>)"
#: src/gettext_strings.rs:50
msgid ""
"This key contains {{ count_revoked }} revoked identities, which are not "
"published. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
@@ -338,11 +260,9 @@ msgstr ""
"gepubliceerd. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Waarom?"
"</a>)"
#: src/gettext_strings.rs:51
msgid "Your keys have been successfully uploaded:"
msgstr "Uw sleutels zijn succesvol geüpload:"
#: src/gettext_strings.rs:52
msgid ""
"<strong>Note:</strong> To make keys searchable by email address, you must "
"upload them individually."
@@ -350,11 +270,9 @@ msgstr ""
"<strong>Opmerking:</strong> Om sleutels doorzoekbaar te maken op e-"
"mailadres, moet u deze afzonderlijk uploaden."
#: src/gettext_strings.rs:53
msgid "Verifying your email address…"
msgstr "Uw e-mailadres verifiëren..."
#: src/gettext_strings.rs:54
msgid ""
"If the process doesn't complete after a few seconds, please <input type="
"\"submit\" class=\"textbutton\" value=\"click here\" />."
@@ -362,15 +280,12 @@ msgstr ""
"Als het proces na enkele seconden niet is voltooid, <input type=\"submit\" "
"class=\"textbutton\" value=\"click here\" /> alstublieft."
#: src/gettext_strings.rs:56
msgid "Manage your key on {{domain}}"
msgstr "Beheer uw sleutel op {{domain}}"
#: src/gettext_strings.rs:58
msgid "Hi,"
msgstr "Hallo,"
#: src/gettext_strings.rs:59
msgid ""
"This is an automated message from <a href=\"{{base_uri}}\" style=\"text-"
"decoration:none; color: #333\">{{domain}}</a>."
@@ -378,15 +293,12 @@ msgstr ""
"Dit is een automatisch bericht van <a href=\"{{base_uri}}\" style=\"text-"
"decoration:none; color: #333\">{{domain}}</a>."
#: src/gettext_strings.rs:60
msgid "If you didn't request this message, please ignore it."
msgstr "Als je dit bericht niet hebt opgevraagd, negeer het dan."
#: src/gettext_strings.rs:61
msgid "OpenPGP key: <tt>{{primary_fp}}</tt>"
msgstr "OpenPGP sleutel: <tt>{{primary_fp}}</tt>"
#: src/gettext_strings.rs:62
msgid ""
"To manage and delete listed addresses on this key, please follow the link "
"below:"
@@ -394,7 +306,6 @@ msgstr ""
"Om de vermelde adressen op deze sleutel te beheren en/of te verwijderen, "
"volg de onderstaande link:"
#: src/gettext_strings.rs:63
msgid ""
"You can find more info at <a href=\"{{base_uri}}/about\">{{domain}}/about</"
"a>."
@@ -402,27 +313,21 @@ msgstr ""
"Je kunt meer informatie vinden op<a href=\"{{base_uri}}/about\"> {{domain}}/"
"over</a>."
#: src/gettext_strings.rs:64
msgid "distributing OpenPGP keys since 2019"
msgstr "distributie van OpenPGP-sleutels sinds 2019"
#: src/gettext_strings.rs:67
msgid "This is an automated message from {{domain}}."
msgstr "Dit is een automatisch bericht van {{domain}}."
#: src/gettext_strings.rs:69
msgid "OpenPGP key: {{primary_fp}}"
msgstr "OpenPGP sleutel: {{primary_fp}}"
#: src/gettext_strings.rs:71
msgid "You can find more info at {{base_uri}}/about"
msgstr "Je kunt meer informatie vinden op {{base_uri}}/over"
#: src/gettext_strings.rs:74
msgid "Verify {{userid}} for your key on {{domain}}"
msgstr "Verifieer {{userid}} voor uw sleutel op {{domain}}"
#: src/gettext_strings.rs:80
msgid ""
"To let others find this key from your email address \"<a rel=\"nofollow\" "
"href=\"#\" style=\"text-decoration:none; color: #333\">{{userid}}</a>\", "
@@ -432,7 +337,6 @@ msgstr ""
"e-mailadres \"<a rel=\"nofollow\" href=\"#\" style=\"text-decoration:none; "
"color: #333\">{{userid}}</a>\":"
#: src/gettext_strings.rs:88
msgid ""
"To let others find this key from your email address \"{{userid}}\",\n"
"please follow the link below:"
@@ -440,43 +344,58 @@ msgstr ""
"Om anderen deze sleutel te laten vinden van uw e-mailadres \"{{userid}}\",\n"
"klik op de onderstaande link:"
#: src/web/manage.rs:103
msgid "No key found for fingerprint {}"
msgstr "Geen sleutel voor adres: {}"
msgid "No key found for key id {}"
msgstr "Geen sleutel voor adres: {}"
msgid "No key found for email address {}"
msgstr "Geen sleutel voor adres: {}"
msgid "Search by Short Key ID is not supported."
msgstr ""
msgid "Invalid search query."
msgstr ""
msgctxt "Subject for verification email, {0} = userid, {1} = keyserver domain"
msgid "Verify {0} for your key on {1}"
msgstr "Verifieer {0} voor uw sleutel op {1}"
msgctxt "Subject for manage email, {} = keyserver domain"
msgid "Manage your key on {}"
msgstr "Beheer uw sleutel op {}"
msgid "This link is invalid or expired"
msgstr "Deze link is ongeldig of verlopen"
#: src/web/manage.rs:129
msgid "Malformed address: {address}"
msgstr "Ongeldig adres: {adres}"
#, fuzzy
msgid "Malformed address: {}"
msgstr "Ongeldig adres: {}"
#: src/web/manage.rs:136
msgid "No key for address: {address}"
msgstr "Geen sleutel voor adres: {address}"
#, fuzzy
msgid "No key for address: {}"
msgstr "Geen sleutel voor adres: {}"
#: src/web/manage.rs:152
msgid "A request has already been sent for this address recently."
msgstr "Er is onlangs al een verzoek voor dit adres verzonden."
#: src/web/vks.rs:111
msgid "Parsing of key data failed."
msgstr "Ontleden van sleutel gegevens is mislukt."
#: src/web/vks.rs:120
msgid "Whoops, please don't upload secret keys!"
msgstr "Oeps, upload geen geheime sleutels!"
#: src/web/vks.rs:133
msgid "No key uploaded."
msgstr "Geen sleutel geüpload."
#: src/web/vks.rs:177
msgid "Error processing uploaded key."
msgstr "Fout bij het verwerken van geüploade sleutel."
#: src/web/vks.rs:247
msgid "Upload session expired. Please try again."
msgstr "Uploadsessie is verlopen. Probeer het alstublieft opnieuw."
#: src/web/vks.rs:284
msgid "Invalid verification link."
msgstr "Ongeldige verificatielink."

View File

@@ -17,58 +17,21 @@ msgstr ""
"%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n"
"%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
#: src/mail.rs:107
msgctxt "Subject for verification email"
msgid "Verify {userid} for your key on {domain}"
msgstr "Zweryfikuj {userid} dla twojego klucza na {domain}"
#: src/mail.rs:140
msgctxt "Subject for manage email"
msgid "Manage your key on {domain}"
msgstr "Zarządzaj swoim kluczem na {domain}"
#: src/i18n_helpers.rs:8
msgid "No key found for fingerprint {fingerprint}"
msgstr ""
#: src/i18n_helpers.rs:10
msgid "No key found for key id {key_id}"
msgstr ""
#: src/i18n_helpers.rs:12
#, fuzzy
msgid "No key found for email address {email}"
msgstr "Brak klucza dla adresu: {address}"
#: src/i18n_helpers.rs:13
msgid "Search by Short Key ID is not supported."
msgstr ""
#: src/i18n_helpers.rs:14
msgid "Invalid search query."
msgstr ""
#: src/gettext_strings.rs:4
msgid "Error"
msgstr "Błąd"
#: src/gettext_strings.rs:5
msgid "Looks like something went wrong :("
msgstr "Wygląda na to, że coś poszło nie tak :("
#: src/gettext_strings.rs:6
msgid "Error message: {{ internal_error }}"
msgstr "Komunikat błędu: {{ internal_error }}"
#: src/gettext_strings.rs:7
msgid "There was an error with your request:"
msgstr "Podczas przetwarzania twojego zapytania wystąpił błąd:"
#: src/gettext_strings.rs:8
msgid "We found an entry for <span class=\"email\">{{ query }}</span>:"
msgstr "Znaleźliśmy wpis dla <span class=\"email\">{{ query }}</span>:"
#: src/gettext_strings.rs:9
msgid ""
"<strong>Hint:</strong> It's more convenient to use <span class=\"brand"
"\">keys.openpgp.org</span> from your OpenPGP software.<br /> Take a look at "
@@ -78,19 +41,15 @@ msgstr ""
"openpgp.org</span> z twojego oprogramowania OpenPGP.<br /> Sprawdź nasz <a "
"href=\"/about/usage\">poradnik</a> aby dowiedzieć się więcej."
#: src/gettext_strings.rs:10
msgid "debug info"
msgstr "informacje diagnostyczne"
#: src/gettext_strings.rs:11
msgid "Search by Email Address / Key ID / Fingerprint"
msgstr "Wyszukaj po adresie e-mail / ID klucza / odcisku"
#: src/gettext_strings.rs:12
msgid "Search"
msgstr "Wyszukaj"
#: src/gettext_strings.rs:13
msgid ""
"You can also <a href=\"/upload\">upload</a> or <a href=\"/manage\">manage</"
"a> your key."
@@ -98,29 +57,23 @@ msgstr ""
"Możesz także <a href=\"/upload\">wgrać</a> lub <a href=\"/manage"
"\">zarządzać</a> swoim kluczem."
#: src/gettext_strings.rs:14
msgid "Find out more <a href=\"/about\">about this service</a>."
msgstr "Dowiedz się więcej <a href=\"/about\">o tej usłudze</a>."
#: src/gettext_strings.rs:15
msgid "News:"
msgstr "Nowości:"
#: src/gettext_strings.rs:16
msgid ""
"<a href=\"/about/news#2019-11-12-celebrating-100k\">Celebrating 100.000 "
"verified addresses! 📈</a> (2019-11-12)"
msgstr ""
#: src/gettext_strings.rs:17
msgid "v{{ version }} built from"
msgstr "v{{ version }} zbudowana z "
#: src/gettext_strings.rs:18
msgid "Powered by <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>"
msgstr "Używa <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>"
#: src/gettext_strings.rs:19
msgid ""
"Background image retrieved from <a href=\"https://www.toptal.com/designers/"
"subtlepatterns/subtle-grey/\">Subtle Patterns</a> under CC BY-SA 3.0"
@@ -128,23 +81,18 @@ msgstr ""
"Tło pobrane z <a href=\"https://www.toptal.com/designers/subtlepatterns/"
"subtle-grey/\">Subtle Patterns</a> na licencji CC BY-SA 3.0"
#: src/gettext_strings.rs:20
msgid "Maintenance Mode"
msgstr "Tryb Serwisowania"
#: src/gettext_strings.rs:21
msgid "Manage your key"
msgstr "Zarządzaj kluczem"
#: src/gettext_strings.rs:22
msgid "Enter any verified email address for your key"
msgstr "Wpisz dowolny zweryfikowany adres swojego klucza"
#: src/gettext_strings.rs:23
msgid "Send link"
msgstr "Wyślij łącze"
#: src/gettext_strings.rs:24
msgid ""
"We will send you an email with a link you can use to remove any of your "
"email addresses from search."
@@ -152,7 +100,6 @@ msgstr ""
"Wyślemy ci e-mail z łączem którego możesz użyć do usunięcia dowolnego adresu "
"z wyszukiwania."
#: src/gettext_strings.rs:25
msgid ""
"Managing the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
@@ -160,16 +107,13 @@ msgstr ""
"Zarządzanie kluczem <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>"
#: src/gettext_strings.rs:26
msgid "Your key is published with the following identity information:"
msgstr ""
"Twój klucz został opublikowany z następującymi informacjami tożsamości:"
#: src/gettext_strings.rs:27
msgid "Delete"
msgstr "Usuń"
#: src/gettext_strings.rs:28
msgid ""
"Clicking \"delete\" on any address will remove it from this key. It will no "
"longer appear in a search.<br /> To add another address, <a href=\"/upload"
@@ -179,7 +123,6 @@ msgstr ""
"on wyświetlany podczas wyszukiwania.<br /> Aby dodać inny adres <a href=\"/"
"upload\">wyślij</a> klucz ponownie."
#: src/gettext_strings.rs:29
msgid ""
"Your key is published as only non-identity information. (<a href=\"/about\" "
"target=\"_blank\">What does this mean?</a>)"
@@ -187,11 +130,9 @@ msgstr ""
"Twój klucz jest opublikowany bez informacji o tożsamości. (<a href=\"/about"
"\" target=\"_blank\">Co to oznacza?</a>)"
#: src/gettext_strings.rs:30
msgid "To add an address, <a href=\"/upload\">upload</a> the key again."
msgstr "Aby dodać adres, <a href=\"/upload\">wyślij</a> klucz ponownie."
#: src/gettext_strings.rs:31
msgid ""
"We have sent an email with further instructions to <span class=\"email"
"\">{{ address }}</span>."
@@ -199,11 +140,9 @@ msgstr ""
"Wysłaliśmy e-mail z dalszymi instrukcjami pod <span class=\"email"
"\">{{ address }}</span>."
#: src/gettext_strings.rs:32
msgid "This address has already been verified."
msgstr "Adres został już zweryfikowany."
#: src/gettext_strings.rs:33
msgid ""
"Your key <span class=\"fingerprint\">{{ key_fpr }}</span> is now published "
"for the identity <a href=\"{{userid_link}}\" target=\"_blank\"><span class="
@@ -213,15 +152,12 @@ msgstr ""
"opublikowany dla tożsamości <a href=\"{{userid_link}}\" target=\"_blank"
"\"><span class=\"email\">{{ userid }}</span></a>."
#: src/gettext_strings.rs:34
msgid "Upload your key"
msgstr "Wyślij swój klucz"
#: src/gettext_strings.rs:35
msgid "Upload"
msgstr "Wyślij"
#: src/gettext_strings.rs:36
msgid ""
"Need more info? Check our <a target=\"_blank\" href=\"/about\">intro</a> and "
"<a target=\"_blank\" href=\"/about/usage\">usage guide</a>."
@@ -230,7 +166,6 @@ msgstr ""
"about\">wprowadzenie</a> oraz <a target=\"_blank\" href=\"/about/usage"
"\">instrukcję użytkowania</a>."
#: src/gettext_strings.rs:37
msgid ""
"You uploaded the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
@@ -238,11 +173,9 @@ msgstr ""
"Wysłałeś klucz <span class=\"fingerprint\"><a href=\"{{ key_link }}\" target="
"\"_blank\">{{ key_fpr }}</a></span>."
#: src/gettext_strings.rs:38
msgid "This key is revoked."
msgstr "Ten klucz jest unieważniony."
#: src/gettext_strings.rs:39
msgid ""
"It is published without identity information and can't be made available for "
"search by email address (<a href=\"/about\" target=\"_blank\">what does this "
@@ -252,7 +185,6 @@ msgstr ""
"pomocą adresu e-mail (<a href=\"/about\" target=\"_blank\">co to znaczy?</"
"a>)."
#: src/gettext_strings.rs:40
msgid ""
"This key is now published with the following identity information (<a href="
"\"/about\" target=\"_blank\">what does this mean?</a>):"
@@ -260,11 +192,9 @@ msgstr ""
"Ten klucz jest teraz opublikowany z następującymi informacjami tożsamości "
"(<a href=\"/about\" target=\"_blank\">co to znaczy?</a>);"
#: src/gettext_strings.rs:41
msgid "Published"
msgstr "Opublikowany"
#: src/gettext_strings.rs:42
msgid ""
"This key is now published with only non-identity information. (<a href=\"/"
"about\" target=\"_blank\">What does this mean?</a>)"
@@ -272,7 +202,6 @@ msgstr ""
"Ten klucz jest opublikowany bez informacji o tożsamości (<a href=\"/about\" "
"target=\"_blank\">Co to oznacza?</a>)"
#: src/gettext_strings.rs:43
msgid ""
"To make the key available for search by email address, you can verify it "
"belongs to you:"
@@ -280,11 +209,9 @@ msgstr ""
"Aby można było wyszukać ten klucz używając adresu e-mail, zweryfikuj, że "
"należy do ciebie:"
#: src/gettext_strings.rs:44
msgid "Verification Pending"
msgstr "Czekam na weryfikację"
#: src/gettext_strings.rs:45
msgid ""
"<strong>Note:</strong> Some providers delay emails for up to 15 minutes to "
"prevent spam. Please be patient."
@@ -292,11 +219,9 @@ msgstr ""
"<strong>Informacja:</strong> Niektórzy dostawcy poczty opóźniają e-maile do "
"15 minut aby zapobiegać spamowi. Proszę być cierpliwym."
#: src/gettext_strings.rs:46
msgid "Send Verification Email"
msgstr "Wyślij E-mail Weryfikacyjny"
#: src/gettext_strings.rs:47
msgid ""
"This key contains one identity that could not be parsed as an email address."
"<br /> This identity can't be published on <span class=\"brand\">keys."
@@ -308,7 +233,6 @@ msgstr ""
"\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-email-uids\" target="
"\"_blank\">Dlaczego?</a>)"
#: src/gettext_strings.rs:48
msgid ""
"This key contains {{ count_unparsed }} identities that could not be parsed "
"as an email address.<br /> These identities can't be published on <span "
@@ -320,7 +244,6 @@ msgstr ""
"<span class=\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-"
"email-uids\" target=\"_blank\">Dlaczego?</a>)"
#: src/gettext_strings.rs:49
msgid ""
"This key contains one revoked identity, which is not published. (<a href=\"/"
"about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
@@ -329,7 +252,6 @@ msgstr ""
"opublikowana. (<a href=\"/about/faq#revoked-uids\" target=\"_blank"
"\">Dlaczego?</a>)"
#: src/gettext_strings.rs:50
msgid ""
"This key contains {{ count_revoked }} revoked identities, which are not "
"published. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
@@ -338,11 +260,9 @@ msgstr ""
"zostały opublikowane. (<a href=\"/about/faq#revoked-uids\" target=\"_blank"
"\">Dlaczego?</a>)"
#: src/gettext_strings.rs:51
msgid "Your keys have been successfully uploaded:"
msgstr "Twoje klucze zostały pomyślnie wysłane:"
#: src/gettext_strings.rs:52
msgid ""
"<strong>Note:</strong> To make keys searchable by email address, you must "
"upload them individually."
@@ -350,11 +270,9 @@ msgstr ""
"<strong>Informacja:</strong> Aby można było wyszukać kluczy za pomocą adresu "
"e-mail, wyślij je pojedynczo."
#: src/gettext_strings.rs:53
msgid "Verifying your email address…"
msgstr "Weryfikowanie twojego adresu e-mail..."
#: src/gettext_strings.rs:54
msgid ""
"If the process doesn't complete after a few seconds, please <input type="
"\"submit\" class=\"textbutton\" value=\"click here\" />."
@@ -362,15 +280,12 @@ msgstr ""
"Jeśli proces nie zakończy się po kilku sekundach proszę <input type=\"submit"
"\" class=\"textbutton\" value=\"kliknij tutaj\"/>."
#: src/gettext_strings.rs:56
msgid "Manage your key on {{domain}}"
msgstr "Zarządzaj swoim kluczem na {{domain}}"
#: src/gettext_strings.rs:58
msgid "Hi,"
msgstr "Cześć,"
#: src/gettext_strings.rs:59
msgid ""
"This is an automated message from <a href=\"{{base_uri}}\" style=\"text-"
"decoration:none; color: #333\">{{domain}}</a>."
@@ -378,21 +293,17 @@ msgstr ""
"To jest automatyczna wiadomość od <a href=\"{{base_uri}}\" style=\"text-"
"decoration:none; color: #333\">{{domain}}</a>."
#: src/gettext_strings.rs:60
msgid "If you didn't request this message, please ignore it."
msgstr "Jeśli nie prosiłeś o tą wiadomość, zignorują ją."
#: src/gettext_strings.rs:61
msgid "OpenPGP key: <tt>{{primary_fp}}</tt>"
msgstr "Klucz OpenPGP: <tt>{{primary_fp}}</tt>"
#: src/gettext_strings.rs:62
msgid ""
"To manage and delete listed addresses on this key, please follow the link "
"below:"
msgstr "Aby zarządzać adresami na kluczu lub je usunąć, użyj tego łącza:"
#: src/gettext_strings.rs:63
msgid ""
"You can find more info at <a href=\"{{base_uri}}/about\">{{domain}}/about</"
"a>."
@@ -400,27 +311,21 @@ msgstr ""
"Więcej informacji znajdziesz na <a href=\"{{base_uri}}/about\">{{domain}}/"
"about</a>."
#: src/gettext_strings.rs:64
msgid "distributing OpenPGP keys since 2019"
msgstr "dostarczanie kluczy OpenPGP od 2019"
#: src/gettext_strings.rs:67
msgid "This is an automated message from {{domain}}."
msgstr "To automatyczna wiadomość z {{domain}}."
#: src/gettext_strings.rs:69
msgid "OpenPGP key: {{primary_fp}}"
msgstr "Klucz OpenPGP: {{primary_fp}}"
#: src/gettext_strings.rs:71
msgid "You can find more info at {{base_uri}}/about"
msgstr "Znajdziesz więcej informacji na {{base_uri}}/about"
#: src/gettext_strings.rs:74
msgid "Verify {{userid}} for your key on {{domain}}"
msgstr "Zweryfikuj {{userid}} dla klucza na {{domain}}"
#: src/gettext_strings.rs:80
msgid ""
"To let others find this key from your email address \"<a rel=\"nofollow\" "
"href=\"#\" style=\"text-decoration:none; color: #333\">{{userid}}</a>\", "
@@ -430,7 +335,6 @@ msgstr ""
"href=\"#\" style=\"text-decoration:none; color: #333\">{{userid}}</a>\", "
"proszę kliknąć w łącze poniżej:"
#: src/gettext_strings.rs:88
msgid ""
"To let others find this key from your email address \"{{userid}}\",\n"
"please follow the link below:"
@@ -438,43 +342,58 @@ msgstr ""
"Aby inni mogli wyszukać twój klucz przez adres e-mail \"{{userid}}\", proszę "
"kliknąć w łącze poniżej:"
#: src/web/manage.rs:103
msgid "No key found for fingerprint {}"
msgstr "Brak klucza dla adresu: {}"
msgid "No key found for key id {}"
msgstr "Brak klucza dla adresu: {}"
msgid "No key found for email address {}"
msgstr "Brak klucza dla adresu: {}"
msgid "Search by Short Key ID is not supported."
msgstr ""
msgid "Invalid search query."
msgstr ""
msgctxt "Subject for verification email, {0} = userid, {1} = keyserver domain"
msgid "Verify {0} for your key on {1}"
msgstr "Zweryfikuj {0} dla twojego klucza na {1}"
msgctxt "Subject for manage email, {} = keyserver domain"
msgid "Manage your key on {}"
msgstr "Zarządzaj swoim kluczem na {}"
msgid "This link is invalid or expired"
msgstr "Łącze jest nieważne lub wygasło"
#: src/web/manage.rs:129
msgid "Malformed address: {address}"
msgstr "Nieprawidłowy adres: {address}"
#, fuzzy
msgid "Malformed address: {}"
msgstr "Nieprawidłowy adres: {}"
#: src/web/manage.rs:136
msgid "No key for address: {address}"
msgstr "Brak klucza dla adresu: {address}"
#, fuzzy
msgid "No key for address: {}"
msgstr "Brak klucza dla adresu: {}"
#: src/web/manage.rs:152
msgid "A request has already been sent for this address recently."
msgstr "Zapytanie o ten adres zostało niedawno wysłane."
#: src/web/vks.rs:111
msgid "Parsing of key data failed."
msgstr "Przetwarzanie danych klucza się nie powiodło."
#: src/web/vks.rs:120
msgid "Whoops, please don't upload secret keys!"
msgstr "Ups, nie wysyłaj kluczy prywatnych!"
#: src/web/vks.rs:133
msgid "No key uploaded."
msgstr "Klucz nie został wysłany."
#: src/web/vks.rs:177
msgid "Error processing uploaded key."
msgstr "Błąd przetwarzania wysłanego klucza."
#: src/web/vks.rs:247
msgid "Upload session expired. Please try again."
msgstr "Sesja wysyłania wygasła. Spróbuj ponownie."
#: src/web/vks.rs:284
msgid "Invalid verification link."
msgstr "Nieprawidłowy link weryfikacyjny."

403
po/hagrid/ro.po Normal file
View File

@@ -0,0 +1,403 @@
#
# Translators:
# Simona Iacob <s@zp1.net>, 2021
#
msgid ""
msgstr ""
"Project-Id-Version: hagrid\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2019-09-27 18:05+0000\n"
"Last-Translator: Simona Iacob <s@zp1.net>, 2021\n"
"Language-Team: Romanian (https://www.transifex.com/otf/teams/102430/ro/)\n"
"Language: ro\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?"
"2:1));\n"
msgid "Error"
msgstr "Eroare"
msgid "Looks like something went wrong :("
msgstr "Se pare că ceva a mers prost :("
msgid "Error message: {{ internal_error }}"
msgstr "Mesaj de eroare: {{ internal_error }}"
msgid "There was an error with your request:"
msgstr "A existat o eroare în cererea dumneavoastră:"
msgid "We found an entry for <span class=\"email\">{{ query }}</span>:"
msgstr "Am găsit o intrare pentru <span class=\"email\">{{ query }}</span>:"
msgid ""
"<strong>Hint:</strong> It's more convenient to use <span class=\"brand"
"\">keys.openpgp.org</span> from your OpenPGP software.<br /> Take a look at "
"our <a href=\"/about/usage\">usage guide</a> for details."
msgstr ""
"<strong>Indicație:</strong> Este mai convenabil să utilizați <span class="
"\"brand\">keys.openpgp.org</span> din software-ul OpenPGP.<br /> Consultați "
"ghidul nostru <a href=\"/about/usage\">de utilizare</a> pentru detalii."
msgid "debug info"
msgstr "informații de depanare"
msgid "Search by Email Address / Key ID / Fingerprint"
msgstr "Căutare după adresa de e-mail / ID cheie / amprentă digitală"
msgid "Search"
msgstr "Căutare"
msgid ""
"You can also <a href=\"/upload\">upload</a> or <a href=\"/manage\">manage</"
"a> your key."
msgstr ""
"De asemenea, puteți <a href=\"/upload\">să vă încărcați</a> ori <a href=\"/"
"manage\">să vă gestionați</a> cheia."
msgid "Find out more <a href=\"/about\">about this service</a>."
msgstr "Aflați mai multe <a href=\"/about\">despre acest serviciu</a>."
msgid "News:"
msgstr "Știri:"
msgid ""
"<a href=\"/about/news#2019-11-12-celebrating-100k\">Celebrating 100.000 "
"verified addresses! 📈</a> (2019-11-12)"
msgstr ""
"<a href=\"/about/news#2019-11-12-celebrating-100k\">Sărbătorim 100.000 de "
"adrese verificate! 📈</a> (2019-11-12)"
msgid "v{{ version }} built from"
msgstr "v{{ version }} construit din"
msgid "Powered by <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>"
msgstr "Alimentat de <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>"
msgid ""
"Background image retrieved from <a href=\"https://www.toptal.com/designers/"
"subtlepatterns/subtle-grey/\">Subtle Patterns</a> under CC BY-SA 3.0"
msgstr ""
"Imagine de fundal preluată de la <a href=\"https://www.toptal.com/designers/"
"subtlepatterns/subtle-grey/\">Subtle Patterns</a> sub CC BY-SA 3.0"
msgid "Maintenance Mode"
msgstr "Modul de întreținere"
msgid "Manage your key"
msgstr "Gestionați cheia dumneavoastră"
msgid "Enter any verified email address for your key"
msgstr ""
"Introduceți orice adresă de e-mail verificată pentru cheia dumneavoastră"
msgid "Send link"
msgstr "Trimite link"
msgid ""
"We will send you an email with a link you can use to remove any of your "
"email addresses from search."
msgstr ""
"Vă vom trimite un e-mail cu un link pe care îl puteți utiliza pentru a "
"elimina oricare dintre adresele dvs. de e-mail din căutare."
msgid ""
"Managing the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
msgstr ""
"Gestionarea cheii <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
msgid "Your key is published with the following identity information:"
msgstr "Cheia dvs. este publicată cu următoarele informații de identitate:"
msgid "Delete"
msgstr "Ștergeți"
msgid ""
"Clicking \"delete\" on any address will remove it from this key. It will no "
"longer appear in a search.<br /> To add another address, <a href=\"/upload"
"\">upload</a> the key again."
msgstr ""
"Dacă faceți clic pe \"delete\" pe orice adresă, aceasta va fi eliminată din "
"această cheie. Aceasta nu va mai apărea într-o căutare.<br /> Pentru a "
"adăuga o altă adresă, <a href=\"/upload\">încărcați</a> din nou cheia."
msgid ""
"Your key is published as only non-identity information. (<a href=\"/about\" "
"target=\"_blank\">What does this mean?</a>)"
msgstr ""
"Cheia dvs. este publicată doar ca informație neidentitară. (<a href=\"/about"
"\" target=\"_blank\">Ce înseamnă acest lucru?</a>)"
msgid "To add an address, <a href=\"/upload\">upload</a> the key again."
msgstr ""
"Pentru a adăuga o adresă, <a href=\"/upload\">încărcați</a> din nou cheia."
msgid ""
"We have sent an email with further instructions to <span class=\"email"
"\">{{ address }}</span>."
msgstr ""
"Am trimis un e-mail cu instrucțiuni suplimentare la <span class=\"email"
"\">{{ address }}</span>."
msgid "This address has already been verified."
msgstr "Această adresă a fost deja verificată."
msgid ""
"Your key <span class=\"fingerprint\">{{ key_fpr }}</span> is now published "
"for the identity <a href=\"{{userid_link}}\" target=\"_blank\"><span class="
"\"email\">{{ userid }}</span></a>."
msgstr ""
"Cheia dvs. <span class=\"fingerprint\">{{ key_fpr }}</span> este acum "
"publicată pentru identitate <a href=\"{{userid_link}}\" target=\"_blank"
"\"><span class=\"email\">{{ userid }}</span></a>."
msgid "Upload your key"
msgstr "Încărcați cheia dumneavoastră"
msgid "Upload"
msgstr "Încărcați"
msgid ""
"Need more info? Check our <a target=\"_blank\" href=\"/about\">intro</a> and "
"<a target=\"_blank\" href=\"/about/usage\">usage guide</a>."
msgstr ""
"Aveți nevoie de mai multe informații? Consultați ghidul nostru <a target="
"\"_blank\" href=\"/about\">introductiv</a> și <a target=\"_blank\" href=\"/"
"about/usage\">de utilizare</a>."
msgid ""
"You uploaded the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
msgstr ""
"Ați încărcat cheia <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
msgid "This key is revoked."
msgstr "Această cheie este revocată."
msgid ""
"It is published without identity information and can't be made available for "
"search by email address (<a href=\"/about\" target=\"_blank\">what does this "
"mean?</a>)."
msgstr ""
"Acesta este publicat fără informații privind identitatea și nu poate fi "
"disponibil pentru căutare după adresa de e-mail (<a href=\"/about\" target="
"\"_blank\">ce înseamnă acest lucru?</a>)."
msgid ""
"This key is now published with the following identity information (<a href="
"\"/about\" target=\"_blank\">what does this mean?</a>):"
msgstr ""
"Această cheie este acum publicată cu următoarele informații de identitate "
"(<a href=\"/about\" target=\"_blank\">ce înseamnă acest lucru?</a>):"
msgid "Published"
msgstr "Publicat"
msgid ""
"This key is now published with only non-identity information. (<a href=\"/"
"about\" target=\"_blank\">What does this mean?</a>)"
msgstr ""
"Această cheie este acum publicată doar cu informații care nu au legătură cu "
"identitatea. (<a href=\"/about\" target=\"_blank\">Ce înseamnă acest lucru?</"
"a>)"
msgid ""
"To make the key available for search by email address, you can verify it "
"belongs to you:"
msgstr ""
"Pentru ca cheia să fie disponibilă pentru căutare după adresa de e-mail, "
"puteți verifica dacă vă aparține:"
msgid "Verification Pending"
msgstr "Verificare în așteptare"
msgid ""
"<strong>Note:</strong> Some providers delay emails for up to 15 minutes to "
"prevent spam. Please be patient."
msgstr ""
"<strong>Notă:</strong> Unii furnizori întârzie e-mailurile cu până la 15 "
"minute pentru a preveni spam-ul. Vă rugăm să aveți răbdare."
msgid "Send Verification Email"
msgstr "Trimiteți e-mail de verificare"
msgid ""
"This key contains one identity that could not be parsed as an email address."
"<br /> This identity can't be published on <span class=\"brand\">keys."
"openpgp.org</span>. (<a href=\"/about/faq#non-email-uids\" target=\"_blank"
"\">Why?</a>)"
msgstr ""
"Această cheie conține o identitate care nu a putut fi interpretată ca o "
"adresă de e-mail.<br /> Această identitate nu poate fi publicată pe <span "
"class=\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-email-"
"uids\" target=\"_blank\">De ce?</a>)"
msgid ""
"This key contains {{ count_unparsed }} identities that could not be parsed "
"as an email address.<br /> These identities can't be published on <span "
"class=\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-email-"
"uids\" target=\"_blank\">Why?</a>)"
msgstr ""
"Această cheie conține {{ count_unparsed }} identități care nu au putut fi "
"analizate ca adresă de e-mail.<br /> Aceste identități nu pot fi publicate "
"pe <span class=\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-"
"email-uids\" target=\"_blank\">De ce?</a>)"
msgid ""
"This key contains one revoked identity, which is not published. (<a href=\"/"
"about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
msgstr ""
"Această cheie conține o identitate revocată, care nu este publicată. (<a "
"href=\"/about/faq#revoked-uids\" target=\"_blank\">De ce?</a>)"
msgid ""
"This key contains {{ count_revoked }} revoked identities, which are not "
"published. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
msgstr ""
"Această cheie conține {{ count_revoked }} identități revocate, care nu sunt "
"publicate. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">De ce?</a>)"
msgid "Your keys have been successfully uploaded:"
msgstr "Cheile dvs. au fost încărcate cu succes:"
msgid ""
"<strong>Note:</strong> To make keys searchable by email address, you must "
"upload them individually."
msgstr ""
"<strong>Notă:</strong> Pentru ca cheile să poată fi căutate în funcție de "
"adresa de e-mail, trebuie să le încărcați individual."
msgid "Verifying your email address…"
msgstr "Verificarea adresei dvs. de e-mail..."
msgid ""
"If the process doesn't complete after a few seconds, please <input type="
"\"submit\" class=\"textbutton\" value=\"click here\" />."
msgstr ""
"Dacă procesul nu se finalizează după câteva secunde, vă rugăm să <input type="
"\"submit\" class=\"textbutton\" value=\"click here\" />."
msgid "Manage your key on {{domain}}"
msgstr "Gestionați cheia dvs. pe {{domain}}"
msgid "Hi,"
msgstr "Salut,"
msgid ""
"This is an automated message from <a href=\"{{base_uri}}\" style=\"text-"
"decoration:none; color: #333\">{{domain}}</a>."
msgstr ""
"Acesta este un mesaj automat de la <a href=\"{{base_uri}}\" style=\"text-"
"decoration:none; color: #333\">{{domain}}</a>."
msgid "If you didn't request this message, please ignore it."
msgstr "Dacă nu ați cerut acest mesaj, vă rugăm să îl ignorați."
msgid "OpenPGP key: <tt>{{primary_fp}}</tt>"
msgstr "OpenPGP key: <tt>{{primary_fp}}</tt>"
msgid ""
"To manage and delete listed addresses on this key, please follow the link "
"below:"
msgstr ""
"Pentru a gestiona și șterge adresele listate pe această cheie, vă rugăm să "
"urmați linkul de mai jos:"
msgid ""
"You can find more info at <a href=\"{{base_uri}}/about\">{{domain}}/about</"
"a>."
msgstr ""
"Puteți găsi mai multe informații la <a href=\"{{base_uri}}/about"
"\">{{domain}}/about</a>."
msgid "distributing OpenPGP keys since 2019"
msgstr "distribuirea cheilor OpenPGP din 2019"
msgid "This is an automated message from {{domain}}."
msgstr "Acesta este un mesaj automat de la {{domain}}."
msgid "OpenPGP key: {{primary_fp}}"
msgstr "OpenPGP cheie: {{primary_fp}}"
msgid "You can find more info at {{base_uri}}/about"
msgstr "Puteți găsi mai multe informații la {{base_uri}}/about"
msgid "Verify {{userid}} for your key on {{domain}}"
msgstr "Verificați {{userid}} pentru cheia dvs. pe {{domain}}"
msgid ""
"To let others find this key from your email address \"<a rel=\"nofollow\" "
"href=\"#\" style=\"text-decoration:none; color: #333\">{{userid}}</a>\", "
"please click the link below:"
msgstr ""
"Pentru a permite altora să găsească această cheie de la adresa dvs. de e-"
"mail \"<a rel=\"nofollow\" href=\"#\" style=\"text-decoration:none; color: "
"#333\">{{userid}}</a>\", vă rugăm să faceți clic pe link-ul de mai jos:"
msgid ""
"To let others find this key from your email address \"{{userid}}\",\n"
"please follow the link below:"
msgstr ""
"Pentru a permite altora să găsească această cheie de la adresa dvs. de e-"
"mail \"{{userid}}\",\n"
"vă rugăm să urmați linkul de mai jos:"
msgid "No key found for fingerprint {}"
msgstr "Nu s-a găsit nicio cheie pentru amprenta digitală {}"
msgid "No key found for key id {}"
msgstr "Nu s-a găsit nicio cheie pentru cheia id {}"
msgid "No key found for email address {}"
msgstr "Nu s-a găsit nicio cheie pentru adresa de e-mail {}"
msgid "Search by Short Key ID is not supported."
msgstr "Căutarea după ID-ul cheii scurte nu este acceptată."
msgid "Invalid search query."
msgstr "Interogare de căutare invalidă."
msgctxt "Subject for verification email, {0} = userid, {1} = keyserver domain"
msgid "Verify {0} for your key on {1}"
msgstr "Verificați {0} pentru cheia dvs. pe {1}"
msgctxt "Subject for manage email, {} = keyserver domain"
msgid "Manage your key on {}"
msgstr "Gestionați cheia dvs. pe {}"
msgid "This link is invalid or expired"
msgstr "Acest link nu este valabil sau a expirat"
#, fuzzy
msgid "Malformed address: {}"
msgstr "Adresa malformată: {}"
#, fuzzy
msgid "No key for address: {}"
msgstr "Nu există cheie pentru adresă: {}"
msgid "A request has already been sent for this address recently."
msgstr "O cerere a fost deja trimisă recent pentru această adresă."
msgid "Parsing of key data failed."
msgstr "Analiza datelor cheie a eșuat."
msgid "Whoops, please don't upload secret keys!"
msgstr "Whoops, vă rugăm să nu încărcați chei secrete!"
msgid "No key uploaded."
msgstr "Nu a fost încărcată nicio cheie."
msgid "Error processing uploaded key."
msgstr "Eroare de procesare a cheii încărcate."
msgid "Upload session expired. Please try again."
msgstr "Sesiunea de încărcare a expirat. Vă rugăm să încercați din nou."
msgid "Invalid verification link."
msgstr "Link de verificare invalid."

View File

@@ -18,58 +18,21 @@ msgstr ""
"%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n"
"%100>=11 && n%100<=14)? 2 : 3);\n"
#: src/mail.rs:107
msgctxt "Subject for verification email"
msgid "Verify {userid} for your key on {domain}"
msgstr "Подтверди {userid} для твоего ключа на {domain}"
#: src/mail.rs:140
msgctxt "Subject for manage email"
msgid "Manage your key on {domain}"
msgstr "Натрой свой ключ на {domain}"
#: src/i18n_helpers.rs:8
msgid "No key found for fingerprint {fingerprint}"
msgstr ""
#: src/i18n_helpers.rs:10
msgid "No key found for key id {key_id}"
msgstr ""
#: src/i18n_helpers.rs:12
#, fuzzy
msgid "No key found for email address {email}"
msgstr "Нет ключа для адреса: {address}"
#: src/i18n_helpers.rs:13
msgid "Search by Short Key ID is not supported."
msgstr ""
#: src/i18n_helpers.rs:14
msgid "Invalid search query."
msgstr ""
#: src/gettext_strings.rs:4
msgid "Error"
msgstr "Ошибка"
#: src/gettext_strings.rs:5
msgid "Looks like something went wrong :("
msgstr "Похоже, что-то пошло не так :("
#: src/gettext_strings.rs:6
msgid "Error message: {{ internal_error }}"
msgstr "Сообщение об ошибке: {{ internal_error }}"
#: src/gettext_strings.rs:7
msgid "There was an error with your request:"
msgstr "Запрос привёл к ошибке:"
#: src/gettext_strings.rs:8
msgid "We found an entry for <span class=\"email\">{{ query }}</span>:"
msgstr "Найдена запись для <span class=\"email\">{{ query }}</span>:"
#: src/gettext_strings.rs:9
msgid ""
"<strong>Hint:</strong> It's more convenient to use <span class=\"brand"
"\">keys.openpgp.org</span> from your OpenPGP software.<br /> Take a look at "
@@ -80,19 +43,15 @@ msgstr ""
"подробной информации смотри <a href=\"/about/usage\">руководство "
"пользования</a>."
#: src/gettext_strings.rs:10
msgid "debug info"
msgstr "отладочная информация"
#: src/gettext_strings.rs:11
msgid "Search by Email Address / Key ID / Fingerprint"
msgstr "Поиск по адресу электронной почты / ID ключа / отпечаткам пальцев"
#: src/gettext_strings.rs:12
msgid "Search"
msgstr "Искать"
#: src/gettext_strings.rs:13
msgid ""
"You can also <a href=\"/upload\">upload</a> or <a href=\"/manage\">manage</"
"a> your key."
@@ -100,29 +59,23 @@ msgstr ""
"Вы также можете <a href=\"/upload\">загрузить</a> или <a href=\"/manage"
"\">настроить</a> свой ключ."
#: src/gettext_strings.rs:14
msgid "Find out more <a href=\"/about\">about this service</a>."
msgstr "Узнайте больше <a href=\"/about\">об этом сервисе</a>."
#: src/gettext_strings.rs:15
msgid "News:"
msgstr "Новости:"
#: src/gettext_strings.rs:16
msgid ""
"<a href=\"/about/news#2019-11-12-celebrating-100k\">Celebrating 100.000 "
"verified addresses! 📈</a> (2019-11-12)"
msgstr ""
#: src/gettext_strings.rs:17
msgid "v{{ version }} built from"
msgstr "версия {{ version }} собранная из"
#: src/gettext_strings.rs:18
msgid "Powered by <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>"
msgstr "работающая на <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>"
#: src/gettext_strings.rs:19
msgid ""
"Background image retrieved from <a href=\"https://www.toptal.com/designers/"
"subtlepatterns/subtle-grey/\">Subtle Patterns</a> under CC BY-SA 3.0"
@@ -130,23 +83,18 @@ msgstr ""
"Фоновое изображение, полученное из <a href=\"https://www.toptal.com/"
"designers/subtlepatterns/subtle-grey/\">Subtle Patterns</a> под CC BY-SA 3.0"
#: src/gettext_strings.rs:20
msgid "Maintenance Mode"
msgstr "Режим технического обслуживания"
#: src/gettext_strings.rs:21
msgid "Manage your key"
msgstr "Натрой свой ключ"
#: src/gettext_strings.rs:22
msgid "Enter any verified email address for your key"
msgstr "Введи любой проверенный адрес электронной почты для твоего ключа"
#: src/gettext_strings.rs:23
msgid "Send link"
msgstr "Отправь ссылку"
#: src/gettext_strings.rs:24
msgid ""
"We will send you an email with a link you can use to remove any of your "
"email addresses from search."
@@ -154,7 +102,6 @@ msgstr ""
"Мы вышлем вам письмо со ссылкой, которую вы можете использовать для удаления "
"любого из ваших адресов электронной почты из поиска."
#: src/gettext_strings.rs:25
msgid ""
"Managing the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
@@ -162,15 +109,12 @@ msgstr ""
"Управление ключом <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
#: src/gettext_strings.rs:26
msgid "Your key is published with the following identity information:"
msgstr "Ваш ключ опубликован со следующей идентификационной информацией:"
#: src/gettext_strings.rs:27
msgid "Delete"
msgstr "Удалить"
#: src/gettext_strings.rs:28
msgid ""
"Clicking \"delete\" on any address will remove it from this key. It will no "
"longer appear in a search.<br /> To add another address, <a href=\"/upload"
@@ -180,7 +124,6 @@ msgstr ""
"не будет отображаться в поиске.<br />Чтобы добавить еще один адрес, <a href="
"\"/upload\">загрузи</a>ключ снова."
#: src/gettext_strings.rs:29
msgid ""
"Your key is published as only non-identity information. (<a href=\"/about\" "
"target=\"_blank\">What does this mean?</a>)"
@@ -188,11 +131,9 @@ msgstr ""
"Ключ опубликован как не имеющий идентификационной информации. (<a href=\"/"
"about\" target=\"_blank\">Что это значит?</a>)"
#: src/gettext_strings.rs:30
msgid "To add an address, <a href=\"/upload\">upload</a> the key again."
msgstr "Чтобы добавить адрес, <a href=\"/upload\">загрузи</a> ключ снова."
#: src/gettext_strings.rs:31
msgid ""
"We have sent an email with further instructions to <span class=\"email"
"\">{{ address }}</span>."
@@ -200,11 +141,9 @@ msgstr ""
"Мы отправили электронное письмо с дальнейшими инструкциями на <span class="
"\"email\">{{ address }}</span>."
#: src/gettext_strings.rs:32
msgid "This address has already been verified."
msgstr "Этот адрес был уже проверен."
#: src/gettext_strings.rs:33
msgid ""
"Your key <span class=\"fingerprint\">{{ key_fpr }}</span> is now published "
"for the identity <a href=\"{{userid_link}}\" target=\"_blank\"><span class="
@@ -214,15 +153,12 @@ msgstr ""
"с идентификацией <a href=\"{{userid_link}}\" target=\"_blank\"><span class="
"\"email\">{{ userid }}</span></a>."
#: src/gettext_strings.rs:34
msgid "Upload your key"
msgstr "Загрузка ключа"
#: src/gettext_strings.rs:35
msgid "Upload"
msgstr "Загрузи"
#: src/gettext_strings.rs:36
msgid ""
"Need more info? Check our <a target=\"_blank\" href=\"/about\">intro</a> and "
"<a target=\"_blank\" href=\"/about/usage\">usage guide</a>."
@@ -231,7 +167,6 @@ msgstr ""
"\">введение</a> и <a target=\"_blank\" href=\"/about/usage\">руководство "
"пользования</a>."
#: src/gettext_strings.rs:37
msgid ""
"You uploaded the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
@@ -239,11 +174,9 @@ msgstr ""
"Загружен ключ <span class=\"fingerprint\"><a href=\"{{ key_link }}\" target="
"\"_blank\">{{ key_fpr }}</a></span>."
#: src/gettext_strings.rs:38
msgid "This key is revoked."
msgstr "Этот ключ отозван."
#: src/gettext_strings.rs:39
msgid ""
"It is published without identity information and can't be made available for "
"search by email address (<a href=\"/about\" target=\"_blank\">what does this "
@@ -253,7 +186,6 @@ msgstr ""
"электронному адресу (<a href=\"/about\" target=\"_blank\">что это значит?</"
"a>)."
#: src/gettext_strings.rs:40
msgid ""
"This key is now published with the following identity information (<a href="
"\"/about\" target=\"_blank\">what does this mean?</a>):"
@@ -261,11 +193,9 @@ msgstr ""
"Этот ключ опубликован со следующей идентификационной информацией (<a href=\"/"
"about\" target=\"_blank\">что это значит?</a>):"
#: src/gettext_strings.rs:41
msgid "Published"
msgstr "Опубликован"
#: src/gettext_strings.rs:42
msgid ""
"This key is now published with only non-identity information. (<a href=\"/"
"about\" target=\"_blank\">What does this mean?</a>)"
@@ -273,7 +203,6 @@ msgstr ""
"Этот ключ опубликован без идентификационной информации. (<a href=\"/about\" "
"target=\"_blank\">Что это значит</a>)"
#: src/gettext_strings.rs:43
msgid ""
"To make the key available for search by email address, you can verify it "
"belongs to you:"
@@ -281,11 +210,9 @@ msgstr ""
"Чтобы сделать ключ доступным для поиска по адресу электронной почты, ты "
"можешь подтвердить, что он принадлежит тебе:"
#: src/gettext_strings.rs:44
msgid "Verification Pending"
msgstr "Ожидает подтверждения"
#: src/gettext_strings.rs:45
msgid ""
"<strong>Note:</strong> Some providers delay emails for up to 15 minutes to "
"prevent spam. Please be patient."
@@ -293,11 +220,9 @@ msgstr ""
"<strong>Примечание:</strong> Некоторые провайдеры задерживают рассылку писем "
"до 15 минут для предотвращения спама. Пожалуйста, наберитесь терпения."
#: src/gettext_strings.rs:46
msgid "Send Verification Email"
msgstr "Отправить проверочное письмо"
#: src/gettext_strings.rs:47
msgid ""
"This key contains one identity that could not be parsed as an email address."
"<br /> This identity can't be published on <span class=\"brand\">keys."
@@ -309,7 +234,6 @@ msgstr ""
"<span class=\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-"
"email-uids\" target=\"_blank\">Почему?</a>)"
#: src/gettext_strings.rs:48
msgid ""
"This key contains {{ count_unparsed }} identities that could not be parsed "
"as an email address.<br /> These identities can't be published on <span "
@@ -321,7 +245,6 @@ msgstr ""
"быть опубликованы на <span class=\"brand\">keys.openpgp.org</span>. (<a href="
"\"/about/faq#non-email-uids\" target=\"_blank\">Почему?</a>)"
#: src/gettext_strings.rs:49
msgid ""
"This key contains one revoked identity, which is not published. (<a href=\"/"
"about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
@@ -329,7 +252,6 @@ msgstr ""
"Этот ключ содержит отозванную идентификацию, которая не опубликована. (<a "
"href=\"/about/faq#revoked-uids\" target=\"_blank\">Почему?</a>)"
#: src/gettext_strings.rs:50
msgid ""
"This key contains {{ count_revoked }} revoked identities, which are not "
"published. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
@@ -338,11 +260,9 @@ msgstr ""
"опубликованны. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Почему?"
"</a>)"
#: src/gettext_strings.rs:51
msgid "Your keys have been successfully uploaded:"
msgstr "Твои ключи были успешно загружены:"
#: src/gettext_strings.rs:52
msgid ""
"<strong>Note:</strong> To make keys searchable by email address, you must "
"upload them individually."
@@ -350,11 +270,9 @@ msgstr ""
"<strong>Примечание:</strong> Чтобы сделать ключи доступными для поиска по "
"адресу электронной почты, надо загрузить их по отдельности."
#: src/gettext_strings.rs:53
msgid "Verifying your email address…"
msgstr "Проверка адреса электронной почты..."
#: src/gettext_strings.rs:54
msgid ""
"If the process doesn't complete after a few seconds, please <input type="
"\"submit\" class=\"textbutton\" value=\"click here\" />."
@@ -362,15 +280,12 @@ msgstr ""
"Если процесс не завершится через несколько секунд, пожалуйста <input type="
"\"submit\" class=\"textbutton\" value=\"нажми сюда\"/>."
#: src/gettext_strings.rs:56
msgid "Manage your key on {{domain}}"
msgstr "Натрой свой ключ на {{domain}}"
#: src/gettext_strings.rs:58
msgid "Hi,"
msgstr "Привет,"
#: src/gettext_strings.rs:59
msgid ""
"This is an automated message from <a href=\"{{base_uri}}\" style=\"text-"
"decoration:none; color: #333\">{{domain}}</a>."
@@ -378,15 +293,12 @@ msgstr ""
"Это автоматическое сообщение сообщение от <a href=\"{{base_uri}}\" style="
"\"text-decoration:none; color: #333\">{{domain}}</a>."
#: src/gettext_strings.rs:60
msgid "If you didn't request this message, please ignore it."
msgstr "Если вы не запрашивали это сообщение, проигнорируйте его пожалуйста."
#: src/gettext_strings.rs:61
msgid "OpenPGP key: <tt>{{primary_fp}}</tt>"
msgstr "OpenPGP ключ: <tt>{{primary_fp}}</tt>"
#: src/gettext_strings.rs:62
msgid ""
"To manage and delete listed addresses on this key, please follow the link "
"below:"
@@ -394,7 +306,6 @@ msgstr ""
"Для настройки и удаления перечисленных на этом ключе адресов перейди по "
"ссылке ниже:"
#: src/gettext_strings.rs:63
msgid ""
"You can find more info at <a href=\"{{base_uri}}/about\">{{domain}}/about</"
"a>."
@@ -402,27 +313,21 @@ msgstr ""
"Дополнительную информацию можно найти по адресу <a href=\"{{base_uri}}/about"
"\">{{domain}}/about</a>."
#: src/gettext_strings.rs:64
msgid "distributing OpenPGP keys since 2019"
msgstr "распространение OpenPGP ключей с 2019 года"
#: src/gettext_strings.rs:67
msgid "This is an automated message from {{domain}}."
msgstr "Это автоматическое сообщение сообщение от {{domain}}."
#: src/gettext_strings.rs:69
msgid "OpenPGP key: {{primary_fp}}"
msgstr "OpenPGP ключ: {{primary_fp}}"
#: src/gettext_strings.rs:71
msgid "You can find more info at {{base_uri}}/about"
msgstr "Дополнительную информацию можно найти по адресу {{base_uri}}/about"
#: src/gettext_strings.rs:74
msgid "Verify {{userid}} for your key on {{domain}}"
msgstr "Подтверди {{userid}} для твоего ключа на {{domain}}"
#: src/gettext_strings.rs:80
msgid ""
"To let others find this key from your email address \"<a rel=\"nofollow\" "
"href=\"#\" style=\"text-decoration:none; color: #333\">{{userid}}</a>\", "
@@ -432,7 +337,6 @@ msgstr ""
"почты \"<a rel=\"nofollow\" href=\"#\" style=\"text-decoration:none; color: "
"#333\">{{userid}}</a>\", перейдите по ссылке ниже пожалуйста:"
#: src/gettext_strings.rs:88
msgid ""
"To let others find this key from your email address \"{{userid}}\",\n"
"please follow the link below:"
@@ -441,43 +345,58 @@ msgstr ""
"почты \"{{userid}}\",\n"
"перейдите по ссылке ниже пожалуйста:"
#: src/web/manage.rs:103
msgid "No key found for fingerprint {}"
msgstr "Нет ключа для адреса: {}"
msgid "No key found for key id {}"
msgstr "Нет ключа для адреса: {}"
msgid "No key found for email address {}"
msgstr "Нет ключа для адреса: {}"
msgid "Search by Short Key ID is not supported."
msgstr ""
msgid "Invalid search query."
msgstr ""
msgctxt "Subject for verification email, {0} = userid, {1} = keyserver domain"
msgid "Verify {0} for your key on {1}"
msgstr "Подтверди {0} для твоего ключа на {1}"
msgctxt "Subject for manage email, {} = keyserver domain"
msgid "Manage your key on {}"
msgstr "Натрой свой ключ на {}"
msgid "This link is invalid or expired"
msgstr "Эта ссылка неверна или просрочена"
#: src/web/manage.rs:129
msgid "Malformed address: {address}"
msgstr "Некорректный адрес: {address}"
#, fuzzy
msgid "Malformed address: {}"
msgstr "Некорректный адрес: {}"
#: src/web/manage.rs:136
msgid "No key for address: {address}"
msgstr "Нет ключа для адреса: {address}"
#, fuzzy
msgid "No key for address: {}"
msgstr "Нет ключа для адреса: {}"
#: src/web/manage.rs:152
msgid "A request has already been sent for this address recently."
msgstr "На этот адрес уже был недавно отправлен запрос."
#: src/web/vks.rs:111
msgid "Parsing of key data failed."
msgstr "Не удалось распознать данные ключа."
#: src/web/vks.rs:120
msgid "Whoops, please don't upload secret keys!"
msgstr "Оп-ля, пожалуйста не загружай секретные ключи!"
#: src/web/vks.rs:133
msgid "No key uploaded."
msgstr "Ключ не загружен."
#: src/web/vks.rs:177
msgid "Error processing uploaded key."
msgstr "Ошибка обработки загруженного ключа."
#: src/web/vks.rs:247
msgid "Upload session expired. Please try again."
msgstr "Сессия загрузки просрочена. Попробуй снова пожалуйста."
#: src/web/vks.rs:284
msgid "Invalid verification link."
msgstr "Неверная проверочная ссылка."

401
po/hagrid/sv.po Normal file
View File

@@ -0,0 +1,401 @@
#
# Translators:
# Felicia Jongleur, 2021
#
msgid ""
msgstr ""
"Project-Id-Version: hagrid\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2019-09-27 18:05+0000\n"
"Last-Translator: Felicia Jongleur, 2021\n"
"Language-Team: Swedish (https://www.transifex.com/otf/teams/102430/sv/)\n"
"Language: sv\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "Error"
msgstr "Fel"
msgid "Looks like something went wrong :("
msgstr "Det verkar som att något gick snett :("
msgid "Error message: {{ internal_error }}"
msgstr "Felmeddelande: {{ internal_error }}"
msgid "There was an error with your request:"
msgstr "Det uppstod ett fel med din begäran:"
msgid "We found an entry for <span class=\"email\">{{ query }}</span>:"
msgstr "Vi hittade en post för <span class=\"email\">{{ query }}</span>:"
msgid ""
"<strong>Hint:</strong> It's more convenient to use <span class=\"brand"
"\">keys.openpgp.org</span> from your OpenPGP software.<br /> Take a look at "
"our <a href=\"/about/usage\">usage guide</a> for details."
msgstr ""
"<strong>Tips:</strong> Det är enklare att använda <span class=\"brand\">keys."
"openpgp.org</span> från din OpenPGP-mjukvara.<br /> Kolla in vår <a href=\"/"
"about/usage\">användarguide</a> för mer detaljer."
msgid "debug info"
msgstr "felsökningsinfo"
msgid "Search by Email Address / Key ID / Fingerprint"
msgstr "Sök efter e-postadress / nyckel-ID / fingeravtryck"
msgid "Search"
msgstr "Sök"
msgid ""
"You can also <a href=\"/upload\">upload</a> or <a href=\"/manage\">manage</"
"a> your key."
msgstr ""
"Du kan också <a href=\"/upload\">ladda upp</a> eller <a href=\"/manage"
"\">hantera</a> din nyckel."
msgid "Find out more <a href=\"/about\">about this service</a>."
msgstr "Läs mer <a href=\"/about\">om den här tjänsten</a>."
msgid "News:"
msgstr "Nyheter:"
msgid ""
"<a href=\"/about/news#2019-11-12-celebrating-100k\">Celebrating 100.000 "
"verified addresses! 📈</a> (2019-11-12)"
msgstr ""
"<a href=\"/about/news#2019-11-12-celebrating-100k\">Vi firar 100 000 "
"verifierade adresser! 📈</a> (2019-11-12)"
msgid "v{{ version }} built from"
msgstr "v{{ version }} byggd från"
msgid "Powered by <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>"
msgstr "Drivs av <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>"
msgid ""
"Background image retrieved from <a href=\"https://www.toptal.com/designers/"
"subtlepatterns/subtle-grey/\">Subtle Patterns</a> under CC BY-SA 3.0"
msgstr ""
"Bakgrundsbild hämtad från <a href=\"https://www.toptal.com/designers/"
"subtlepatterns/subtle-grey/\">Subtle Patterns</a> under CC BY-SA 3.0"
msgid "Maintenance Mode"
msgstr "Underhållsläge"
msgid "Manage your key"
msgstr "Hantera din nyckel"
msgid "Enter any verified email address for your key"
msgstr "Ange någon verifierad e-postadress för din nyckel"
msgid "Send link"
msgstr "Skicka länk"
msgid ""
"We will send you an email with a link you can use to remove any of your "
"email addresses from search."
msgstr ""
"Vi kommer att skicka ett e-postmeddelande till dig med en länk som du kan "
"använda för att ta bort någon av dina e-postadresser från sökresultaten."
msgid ""
"Managing the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
msgstr ""
"Hanterar nyckeln <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
msgid "Your key is published with the following identity information:"
msgstr "Din nyckel är publicerad med följande identitetsinformation:"
msgid "Delete"
msgstr "Radera"
msgid ""
"Clicking \"delete\" on any address will remove it from this key. It will no "
"longer appear in a search.<br /> To add another address, <a href=\"/upload"
"\">upload</a> the key again."
msgstr ""
"Att klicka \"radera\" på en adress kommer att ta bort den från den här "
"nyckeln. Den kommer inte längre att visas i en sökning.<br /> För att lägga "
"till en annan adress, <a href=\"/upload\">ladda upp</a>nyckeln igen. "
msgid ""
"Your key is published as only non-identity information. (<a href=\"/about\" "
"target=\"_blank\">What does this mean?</a>)"
msgstr ""
"Din nyckel är nu publicerad som endast icke-identifierbar information. (<a "
"href=\"/about\" target=\"_blank\">Vad betyder detta?</a>)"
msgid "To add an address, <a href=\"/upload\">upload</a> the key again."
msgstr ""
"För att lägga till en adress, <a href=\"/upload\">ladda upp</a> nyckeln igen."
msgid ""
"We have sent an email with further instructions to <span class=\"email"
"\">{{ address }}</span>."
msgstr ""
"Vi har skickat ett e-postmeddelande med vidare instruktioner till <span "
"class=\"email\">{{ address }}</span>."
msgid "This address has already been verified."
msgstr "Den här adressen har redan verifierats."
msgid ""
"Your key <span class=\"fingerprint\">{{ key_fpr }}</span> is now published "
"for the identity <a href=\"{{userid_link}}\" target=\"_blank\"><span class="
"\"email\">{{ userid }}</span></a>."
msgstr ""
"Din nyckel <span class=\"fingerprint\">{{ key_fpr }}</span> är nu publicerad "
"för identiteten <a href=\"{{userid_link}}\" target=\"_blank\"><span class="
"\"email\">{{ userid }}</span></a>."
msgid "Upload your key"
msgstr "Ladda upp din nyckel"
msgid "Upload"
msgstr "Ladda upp"
msgid ""
"Need more info? Check our <a target=\"_blank\" href=\"/about\">intro</a> and "
"<a target=\"_blank\" href=\"/about/usage\">usage guide</a>."
msgstr ""
"Behöver du mer information? Kolla in vår <a target=\"_blank\" href=\"/about"
"\">introduktion</a> och <a target=\"_blank\" href=\"/about/usage"
"\">användarguide</a>."
msgid ""
"You uploaded the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
msgstr ""
"Du laddade upp nyckeln <span class=\"fingerprint\"><a href="
"\"{{ key_link }}\" target=\"_blank\">{{ key_fpr }}</a></span>."
msgid "This key is revoked."
msgstr "Den här nyckeln är återkallad."
msgid ""
"It is published without identity information and can't be made available for "
"search by email address (<a href=\"/about\" target=\"_blank\">what does this "
"mean?</a>)."
msgstr ""
"Den är nu publicerad utan identitetsinformation och kan inte göras "
"tillgänglig för sökning via e-postadress (<a href=\"/about\" target=\"_blank"
"\">vad betyder detta?</a>)."
msgid ""
"This key is now published with the following identity information (<a href="
"\"/about\" target=\"_blank\">what does this mean?</a>):"
msgstr ""
"Den här nyckeln är nu publicerad med följande identitetsinformation (<a href="
"\"/about\" target=\"_blank\">vad betyder detta?</a>):"
msgid "Published"
msgstr "Publicerad"
msgid ""
"This key is now published with only non-identity information. (<a href=\"/"
"about\" target=\"_blank\">What does this mean?</a>)"
msgstr ""
"Den här nyckeln är nu publicerad med endast icke-identifierbar information. "
"(<a href=\"/about\" target=\"_blank\">Vad betyder detta?</a>)"
msgid ""
"To make the key available for search by email address, you can verify it "
"belongs to you:"
msgstr ""
"Du kan verifiera att nyckeln tillhör dig för att göra den sökbar via e-"
"postadress:"
msgid "Verification Pending"
msgstr "Avvaktar verifiering"
msgid ""
"<strong>Note:</strong> Some providers delay emails for up to 15 minutes to "
"prevent spam. Please be patient."
msgstr ""
"<strong>Obs:</strong> Vissa leverantörer fördröjer e-postmeddelanden i upp "
"till 15 minuter för att förhindra spam. Ha tålamod."
msgid "Send Verification Email"
msgstr "Skicka verifieringsmeddelande"
msgid ""
"This key contains one identity that could not be parsed as an email address."
"<br /> This identity can't be published on <span class=\"brand\">keys."
"openpgp.org</span>. (<a href=\"/about/faq#non-email-uids\" target=\"_blank"
"\">Why?</a>)"
msgstr ""
"Den här nyckeln innehåller en identitet som inte kunde behandlas som en e-"
"postadress.<br /> Den här identiteten kan inte publiceras på <span class="
"\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-email-uids\" "
"target=\"_blank\">Varför?</a>)"
msgid ""
"This key contains {{ count_unparsed }} identities that could not be parsed "
"as an email address.<br /> These identities can't be published on <span "
"class=\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-email-"
"uids\" target=\"_blank\">Why?</a>)"
msgstr ""
"Den här nyckeln innehåller {{ count_unparsed }} identiteter som inte kunde "
"behandlas som en e-postadress.<br /> Dessa identiteter kan inte publiceras "
"på <span class=\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-"
"email-uids\" target=\"_blank\">Varför?</a>)"
msgid ""
"This key contains one revoked identity, which is not published. (<a href=\"/"
"about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
msgstr ""
"Den här nyckeln innehåller en återkallad identitet, som inte är publicerad. "
"(<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Varför?</a>)"
msgid ""
"This key contains {{ count_revoked }} revoked identities, which are not "
"published. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
msgstr ""
"Den här nyckeln innehåller {{ count_revoked }} återkallade identiteter, som "
"inte är publicerade. (<a href=\"/about/faq#revoked-uids\" target=\"_blank"
"\">Varför?</a>)"
msgid "Your keys have been successfully uploaded:"
msgstr "Din nycklar har laddats upp:"
msgid ""
"<strong>Note:</strong> To make keys searchable by email address, you must "
"upload them individually."
msgstr ""
"<strong>Obs:</strong> För att göra nycklar sökbara via e-postadress behöver "
"du ladda upp dem en och en."
msgid "Verifying your email address…"
msgstr "Verifierar din e-postadress…"
msgid ""
"If the process doesn't complete after a few seconds, please <input type="
"\"submit\" class=\"textbutton\" value=\"click here\" />."
msgstr ""
"Om processen inte slutförs efter några sekunder, vänligen <input type="
"\"submit\" class=\"textbutton\" value=\"click here\" />."
msgid "Manage your key on {{domain}}"
msgstr "Hantera din nyckel på {{domain}}"
msgid "Hi,"
msgstr "Hej,"
msgid ""
"This is an automated message from <a href=\"{{base_uri}}\" style=\"text-"
"decoration:none; color: #333\">{{domain}}</a>."
msgstr ""
"Detta är ett automatiskt meddelande från <a href=\"{{base_uri}}\" style="
"\"text-decoration:none; color: #333\">{{domain}}</a>."
msgid "If you didn't request this message, please ignore it."
msgstr "Om du inte bad om det här meddelandet, vänligen ignorera det."
msgid "OpenPGP key: <tt>{{primary_fp}}</tt>"
msgstr "OpenPGP-nyckel: <tt>{{primary_fp}}</tt>"
msgid ""
"To manage and delete listed addresses on this key, please follow the link "
"below:"
msgstr ""
"För att hantera och radera listade adresser på den här nyckeln, följ länken "
"nedan:"
msgid ""
"You can find more info at <a href=\"{{base_uri}}/about\">{{domain}}/about</"
"a>."
msgstr ""
"Du kan hitta mer information på <a href=\"{{base_uri}}/about\">{{domain}}/"
"about</a>."
msgid "distributing OpenPGP keys since 2019"
msgstr "distribuerar OpenPGP-nycklar sedan 2019"
msgid "This is an automated message from {{domain}}."
msgstr "Detta är ett automatiskt meddelande från {{domain}}."
msgid "OpenPGP key: {{primary_fp}}"
msgstr "OpenPGP-nyckel: {{primary_fp}}"
msgid "You can find more info at {{base_uri}}/about"
msgstr "Du kan hitta mer information på {{base_uri}}/about"
msgid "Verify {{userid}} for your key on {{domain}}"
msgstr "Verifiera {{userid}} för din nyckel på {{domain}}"
msgid ""
"To let others find this key from your email address \"<a rel=\"nofollow\" "
"href=\"#\" style=\"text-decoration:none; color: #333\">{{userid}}</a>\", "
"please click the link below:"
msgstr ""
"För att låta andra hitta den här nyckeln från din e-postadress \"<a rel="
"\"nofollow\" href=\"#\" style=\"text-decoration:none; color: "
"#333\">{{userid}}</a>\", klicka på länken nedan:"
msgid ""
"To let others find this key from your email address \"{{userid}}\",\n"
"please follow the link below:"
msgstr ""
"För att låta andra hitta den här nyckeln från din e-postadress "
"\"{{userid}}\",\n"
"klicka på länken nedan:"
msgid "No key found for fingerprint {}"
msgstr "Ingen nyckel hittades för fingeravtrycket {}"
msgid "No key found for key id {}"
msgstr "Ingen nyckel hittades för nyckel-ID {}"
msgid "No key found for email address {}"
msgstr "Ingen nyckel hittades för e-postadressen {}"
msgid "Search by Short Key ID is not supported."
msgstr "Att söka efter kort nyckel-ID stöds ej."
msgid "Invalid search query."
msgstr "Ogiltig sökterm."
msgctxt "Subject for verification email, {0} = userid, {1} = keyserver domain"
msgid "Verify {0} for your key on {1}"
msgstr "Verifiera {0} för din nyckel på {1}"
msgctxt "Subject for manage email, {} = keyserver domain"
msgid "Manage your key on {}"
msgstr "Hantera din nyckel på {}"
msgid "This link is invalid or expired"
msgstr "Den här länken är ogiltig eller har upphört att gälla"
#, fuzzy
msgid "Malformed address: {}"
msgstr "Felformaterad adress: {}"
#, fuzzy
msgid "No key for address: {}"
msgstr "Ingen nyckel för adress: {}"
msgid "A request has already been sent for this address recently."
msgstr "En begäran har redan skickats för den här adressen nyligen."
msgid "Parsing of key data failed."
msgstr "Behandling av nyckelinformation misslyckades."
msgid "Whoops, please don't upload secret keys!"
msgstr "Hoppsan, ladda inte upp privata nycklar tack!"
msgid "No key uploaded."
msgstr "Ingen nyckel laddades upp."
msgid "Error processing uploaded key."
msgstr "Fel när uppladdad nyckel bearbetades."
msgid "Upload session expired. Please try again."
msgstr "Uppladdningssession gick ut. Vänligen försök igen."
msgid "Invalid verification link."
msgstr "Ogiltig verifieringslänk."

View File

@@ -1,8 +1,8 @@
#
#
# Translators:
# Vincent Breitmoser <look@my.amazin.horse>, 2020
# T. E. Kalayci <tekrei@gmail.com>, 2021
#
#
msgid ""
msgstr ""
"Project-Id-Version: hagrid\n"
@@ -11,101 +11,58 @@ msgstr ""
"PO-Revision-Date: 2019-09-27 18:05+0000\n"
"Last-Translator: T. E. Kalayci <tekrei@gmail.com>, 2021\n"
"Language-Team: Turkish (https://www.transifex.com/otf/teams/102430/tr/)\n"
"Language: tr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: tr\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: src/mail.rs:107
msgctxt "Subject for verification email"
msgid "Verify {userid} for your key on {domain}"
msgstr "{domain} üzerindeki anahtarınız için {userid} doğrulaması"
#: src/mail.rs:140
msgctxt "Subject for manage email"
msgid "Manage your key on {domain}"
msgstr "{domain} sunucusundaki anahtarınızı yönetin"
#: src/i18n_helpers.rs:8
msgid "No key found for fingerprint {fingerprint}"
msgstr "Parmak izi {fingerprint} için anahtar bulunamadı"
#: src/i18n_helpers.rs:10
msgid "No key found for key id {key_id}"
msgstr "Anahtar kimliği {key_id} için anahtar bulunamadı"
#: src/i18n_helpers.rs:12
msgid "No key found for email address {email}"
msgstr "E-posta adresi {email} için anahtar bulunamadı"
#: src/i18n_helpers.rs:13
msgid "Search by Short Key ID is not supported."
msgstr "Kısa Anahtar Kimliği ile arama desteklenmiyor."
#: src/i18n_helpers.rs:14
msgid "Invalid search query."
msgstr "Geçersiz arama sorgusu."
#: src/gettext_strings.rs:4
msgid "Error"
msgstr "Hata"
#: src/gettext_strings.rs:5
msgid "Looks like something went wrong :("
msgstr "Bir şeyler ters gitti gibi gözüküyor :("
#: src/gettext_strings.rs:6
msgid "Error message: {{ internal_error }}"
msgstr "Hata mesajı: {{ internal_error }}"
#: src/gettext_strings.rs:7
msgid "There was an error with your request:"
msgstr "İsteğinizle ilgili bir hata oluştu:"
#: src/gettext_strings.rs:8
msgid "We found an entry for <span class=\"email\">{{ query }}</span>:"
msgstr "<span class=\"email\">{{ query }}</span> için bir girdi bulundu:"
#: src/gettext_strings.rs:9
msgid ""
"<strong>Hint:</strong> It's more convenient to use <span "
"class=\"brand\">keys.openpgp.org</span> from your OpenPGP software.<br /> "
"Take a look at our <a href=\"/about/usage\">usage guide</a> for details."
"<strong>Hint:</strong> It's more convenient to use <span class=\"brand"
"\">keys.openpgp.org</span> from your OpenPGP software.<br /> Take a look at "
"our <a href=\"/about/usage\">usage guide</a> for details."
msgstr ""
"<strong>İpucu:</strong> <span class=\"brand\">keys.openpgp.org</span> "
"hizmetini OpenPGP yazılımından kullanmak daha elverişlidir.<br />Ayrıntılar "
"için <a href=\"/about/usage\">kullanma kılavuzumuza</a> bakabilirsiniz."
#: src/gettext_strings.rs:10
msgid "debug info"
msgstr "hata ayıklama bilgileri"
#: src/gettext_strings.rs:11
msgid "Search by Email Address / Key ID / Fingerprint"
msgstr "E-posta adresi / Anahtar Kimliği / Parmak İzi ile arama"
#: src/gettext_strings.rs:12
msgid "Search"
msgstr "Ara"
#: src/gettext_strings.rs:13
msgid ""
"You can also <a href=\"/upload\">upload</a> or <a "
"href=\"/manage\">manage</a> your key."
"You can also <a href=\"/upload\">upload</a> or <a href=\"/manage\">manage</"
"a> your key."
msgstr ""
"Ayrıca anahtarınızı <a href=\"/upload\">yükleyebilir</a> veya <a "
"href=\"/manage\">yönetebilirsiniz</a>."
"Ayrıca anahtarınızı <a href=\"/upload\">yükleyebilir</a> veya <a href=\"/"
"manage\">yönetebilirsiniz</a>."
#: src/gettext_strings.rs:14
msgid "Find out more <a href=\"/about\">about this service</a>."
msgstr "<a href=\"/about\">Bu hizmet hakkında</a> daha fazlasını öğrenin."
#: src/gettext_strings.rs:15
msgid "News:"
msgstr "Haberler:"
#: src/gettext_strings.rs:16
msgid ""
"<a href=\"/about/news#2019-11-12-celebrating-100k\">Celebrating 100.000 "
"verified addresses! 📈</a> (2019-11-12)"
@@ -113,41 +70,32 @@ msgstr ""
"<a href=\"/about/news#2019-11-12-celebrating-100k\"> 100.000 doğrulanmış "
"adresi kutluyoruz 📈</a> (2019-11-12)"
#: src/gettext_strings.rs:17
msgid "v{{ version }} built from"
msgstr "v{{ version }} dayandığı sürüm"
#: src/gettext_strings.rs:18
msgid "Powered by <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>"
msgstr "<a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>desteklidir"
#: src/gettext_strings.rs:19
msgid ""
"Background image retrieved from <a "
"href=\"https://www.toptal.com/designers/subtlepatterns/subtle-grey/\">Subtle"
" Patterns</a> under CC BY-SA 3.0"
"Background image retrieved from <a href=\"https://www.toptal.com/designers/"
"subtlepatterns/subtle-grey/\">Subtle Patterns</a> under CC BY-SA 3.0"
msgstr ""
"Arkaplan resmi <a href=\"https://www.toptal.com/designers/subtlepatterns"
"/subtle-grey/\">Subtle Patterns</a>tarafından CC BY-SA 3.0 lisansıyla "
"Arkaplan resmi <a href=\"https://www.toptal.com/designers/subtlepatterns/"
"subtle-grey/\">Subtle Patterns</a>tarafından CC BY-SA 3.0 lisansıyla "
"sağlanmıştır."
#: src/gettext_strings.rs:20
msgid "Maintenance Mode"
msgstr "Bakım Modu"
#: src/gettext_strings.rs:21
msgid "Manage your key"
msgstr "Anahtarınızı yönetin"
#: src/gettext_strings.rs:22
msgid "Enter any verified email address for your key"
msgstr "Anahtarınız için doğrulanmış bir e-posta adresi girin"
#: src/gettext_strings.rs:23
msgid "Send link"
msgstr "Bağlantıyı gönder"
#: src/gettext_strings.rs:24
msgid ""
"We will send you an email with a link you can use to remove any of your "
"email addresses from search."
@@ -155,125 +103,107 @@ msgstr ""
"E-posta adreslerinizi aramadan kaldırabilmek için kullanabileceğiniz bir "
"bağlantıyı size e-posta olarak göndereceğiz."
#: src/gettext_strings.rs:25
msgid ""
"Managing the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
msgstr ""
"<span class=\"fingerprint\"><a href=\"{{ key_link }}\" target=\"_blank\">{{ "
"key_fpr }}</a></span> anahtarı yönetiliyor."
"<span class=\"fingerprint\"><a href=\"{{ key_link }}\" target=\"_blank"
"\">{{ key_fpr }}</a></span> anahtarı yönetiliyor."
#: src/gettext_strings.rs:26
msgid "Your key is published with the following identity information:"
msgstr "Anahtarınız aşağıdaki kimlik bilgileriyle yayınlanıyor:"
#: src/gettext_strings.rs:27
msgid "Delete"
msgstr "Sil"
#: src/gettext_strings.rs:28
msgid ""
"Clicking \"delete\" on any address will remove it from this key. It will no "
"longer appear in a search.<br /> To add another address, <a "
"href=\"/upload\">upload</a> the key again."
"longer appear in a search.<br /> To add another address, <a href=\"/upload"
"\">upload</a> the key again."
msgstr ""
"Herhangi bir adreste \"sil\"e tıklamanız durumunda, o adresi anahtardan "
"kaldıracaktır. Aramada artık gözükmeyecektir. <br />Başka bir adres eklemek "
"için anahtarı tekrar <a href=\"/upload\">yükleyin</a> ."
#: src/gettext_strings.rs:29
msgid ""
"Your key is published as only non-identity information. (<a href=\"/about\""
" target=\"_blank\">What does this mean?</a>)"
"Your key is published as only non-identity information. (<a href=\"/about\" "
"target=\"_blank\">What does this mean?</a>)"
msgstr ""
"Anahtarınız kimlik bilgisi içermeden yayınlanıyor. (<a href=\"/about\" "
"target=\"_blank\">Bunun anlamı nedir</a>)"
#: src/gettext_strings.rs:30
msgid "To add an address, <a href=\"/upload\">upload</a> the key again."
msgstr "Bir adres eklemek için anahtarı tekrar <a href=\"/upload\">yükleyin</a>."
#: src/gettext_strings.rs:31
msgid ""
"We have sent an email with further instructions to <span class=\"email\">{{ "
"address }}</span>."
msgstr ""
"Ek yönergeleri içeren bir e-postayı <span class=\"email\">{{ address "
"}}</span> adresine gönderdik."
"Bir adres eklemek için anahtarı tekrar <a href=\"/upload\">yükleyin</a>."
msgid ""
"We have sent an email with further instructions to <span class=\"email"
"\">{{ address }}</span>."
msgstr ""
"Ek yönergeleri içeren bir e-postayı <span class=\"email\">{{ address }}</"
"span> adresine gönderdik."
#: src/gettext_strings.rs:32
msgid "This address has already been verified."
msgstr "Bu adres zaten doğrulanmış."
#: src/gettext_strings.rs:33
msgid ""
"Your key <span class=\"fingerprint\">{{ key_fpr }}</span> is now published "
"for the identity <a href=\"{{userid_link}}\" target=\"_blank\"><span "
"class=\"email\">{{ userid }}</span></a>."
"for the identity <a href=\"{{userid_link}}\" target=\"_blank\"><span class="
"\"email\">{{ userid }}</span></a>."
msgstr ""
"<span class=\"fingerprint\">{{ key_fpr }}</span> anahtarınız şimdi <a "
"href=\"{{userid_link}}\" target=\"_blank\"><span class=\"email\">{{ userid "
"}}</span></a> kimliği için yayınlandı."
"<span class=\"fingerprint\">{{ key_fpr }}</span> anahtarınız şimdi <a href="
"\"{{userid_link}}\" target=\"_blank\"><span class=\"email\">{{ userid }}</"
"span></a> kimliği için yayınlandı."
#: src/gettext_strings.rs:34
msgid "Upload your key"
msgstr "Anahtarınızı yükleyin"
#: src/gettext_strings.rs:35
msgid "Upload"
msgstr "Yükle"
#: src/gettext_strings.rs:36
msgid ""
"Need more info? Check our <a target=\"_blank\" href=\"/about\">intro</a> and"
" <a target=\"_blank\" href=\"/about/usage\">usage guide</a>."
"Need more info? Check our <a target=\"_blank\" href=\"/about\">intro</a> and "
"<a target=\"_blank\" href=\"/about/usage\">usage guide</a>."
msgstr ""
"Daha fazla bilgi için<a target=\"_blank\" href=\"/about\">tanıtım</a> veya "
"<a target=\"_blank\" href=\"/about/usage\">kullanma kılavuzuna</a> "
"bakabilirsiniz."
#: src/gettext_strings.rs:37
msgid ""
"You uploaded the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
msgstr ""
"<span class=\"fingerprint\"><a href=\"{{ key_link }}\" target=\"_blank\">{{ "
"key_fpr }}</a></span> anahtarını yüklediniz."
"<span class=\"fingerprint\"><a href=\"{{ key_link }}\" target=\"_blank"
"\">{{ key_fpr }}</a></span> anahtarını yüklediniz."
#: src/gettext_strings.rs:38
msgid "This key is revoked."
msgstr "Bu anahtar feshedildi."
#: src/gettext_strings.rs:39
msgid ""
"It is published without identity information and can't be made available for"
" search by email address (<a href=\"/about\" target=\"_blank\">what does "
"this mean?</a>)."
"It is published without identity information and can't be made available for "
"search by email address (<a href=\"/about\" target=\"_blank\">what does this "
"mean?</a>)."
msgstr ""
"Kimlik bilgisi olmayan yayınlandı ve e-posta aramasıyla bulunamaz (<a "
"href=\"/about\" target=\"_blank\">bunun anlamı nedir?</a>)"
"Kimlik bilgisi olmayan yayınlandı ve e-posta aramasıyla bulunamaz (<a href="
"\"/about\" target=\"_blank\">bunun anlamı nedir?</a>)"
#: src/gettext_strings.rs:40
msgid ""
"This key is now published with the following identity information (<a "
"href=\"/about\" target=\"_blank\">what does this mean?</a>):"
"This key is now published with the following identity information (<a href="
"\"/about\" target=\"_blank\">what does this mean?</a>):"
msgstr ""
"Bu anahtar şimdi aşağıdaki kimlik bilgisiyle yayınlandı (<a href=\"/about\" "
"target=\"_blank\">bunun anlamı nedir?</a>)"
#: src/gettext_strings.rs:41
msgid "Published"
msgstr "Yayınlandı"
#: src/gettext_strings.rs:42
msgid ""
"This key is now published with only non-identity information. (<a "
"href=\"/about\" target=\"_blank\">What does this mean?</a>)"
"This key is now published with only non-identity information. (<a href=\"/"
"about\" target=\"_blank\">What does this mean?</a>)"
msgstr ""
"Bu anahtar şimdi sadece kimlik olmayan bilgiyle yayınlandı (<a "
"href=\"/about\" target=\"_blank\">bunun anlamı nedir?</a>)"
"Bu anahtar şimdi sadece kimlik olmayan bilgiyle yayınlandı (<a href=\"/about"
"\" target=\"_blank\">bunun anlamı nedir?</a>)"
#: src/gettext_strings.rs:43
msgid ""
"To make the key available for search by email address, you can verify it "
"belongs to you:"
@@ -281,11 +211,9 @@ msgstr ""
"Anahtarı e-posta adresiyle bulunabilir yapmak için, size ait olduğunu "
"doğrulamanız gerekiyor:"
#: src/gettext_strings.rs:44
msgid "Verification Pending"
msgstr "Doğrulama Bekleniyor"
#: src/gettext_strings.rs:45
msgid ""
"<strong>Note:</strong> Some providers delay emails for up to 15 minutes to "
"prevent spam. Please be patient."
@@ -294,23 +222,20 @@ msgstr ""
"amacıyla e-postaları 15 dakikaya kadar geciktirebiliyor. Lütfen sabırla "
"bekleyin."
#: src/gettext_strings.rs:46
msgid "Send Verification Email"
msgstr "Doğrulama E-postasını Gönder"
#: src/gettext_strings.rs:47
msgid ""
"This key contains one identity that could not be parsed as an email "
"address.<br /> This identity can't be published on <span "
"class=\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-email-"
"uids\" target=\"_blank\">Why?</a>)"
"This key contains one identity that could not be parsed as an email address."
"<br /> This identity can't be published on <span class=\"brand\">keys."
"openpgp.org</span>. (<a href=\"/about/faq#non-email-uids\" target=\"_blank"
"\">Why?</a>)"
msgstr ""
"Bu anahtar e-posta adresi olarak çözümlenemeyen bir kimlik içeriyor. <br "
"/>Bu kimlik bilgisi <span class=\"brand\">keys.openpgp.org</span>hizmetinde "
"yayınlanamaz. (<a href=\"/about/faq#non-email-uids\" "
"target=\"_blank\">Neden?</a>)"
"Bu anahtar e-posta adresi olarak çözümlenemeyen bir kimlik içeriyor. <br /"
">Bu kimlik bilgisi <span class=\"brand\">keys.openpgp.org</span>hizmetinde "
"yayınlanamaz. (<a href=\"/about/faq#non-email-uids\" target=\"_blank\">Neden?"
"</a>)"
#: src/gettext_strings.rs:48
msgid ""
"This key contains {{ count_unparsed }} identities that could not be parsed "
"as an email address.<br /> These identities can't be published on <span "
@@ -318,19 +243,17 @@ msgid ""
"uids\" target=\"_blank\">Why?</a>)"
msgstr ""
"Bu anahtar e-posta adresi olarak çözümlenemeyen {{ count_unparsed }} kimlik "
"bilgisi içeriyor. <br />Bu kimlik bilgileri <span "
"class=\"brand\">keys.openpgp.org</span> hizmtinde yayınlanamaz. (<a "
"href=\"/about/faq#non-email-uids\" target=\"_blank\">Neden?</a>)"
"bilgisi içeriyor. <br />Bu kimlik bilgileri <span class=\"brand\">keys."
"openpgp.org</span> hizmtinde yayınlanamaz. (<a href=\"/about/faq#non-email-"
"uids\" target=\"_blank\">Neden?</a>)"
#: src/gettext_strings.rs:49
msgid ""
"This key contains one revoked identity, which is not published. (<a "
"href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
"This key contains one revoked identity, which is not published. (<a href=\"/"
"about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
msgstr ""
"Bu anahtar feshedilmiş, yayınlanmayan bir kimlik içeriyor. (<a "
"href=\"/about/faq#revoked-uids\" target=\"_blank\">Neden?</a>)"
"Bu anahtar feshedilmiş, yayınlanmayan bir kimlik içeriyor. (<a href=\"/about/"
"faq#revoked-uids\" target=\"_blank\">Neden?</a>)"
#: src/gettext_strings.rs:50
msgid ""
"This key contains {{ count_revoked }} revoked identities, which are not "
"published. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
@@ -338,11 +261,9 @@ msgstr ""
"Bu anahtar feshedilmiş, yayınlanmayan {{ count_revoked }} kimlik bilgisi "
"içeriyor. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Neden?</a>)"
#: src/gettext_strings.rs:51
msgid "Your keys have been successfully uploaded:"
msgstr "Anahtarlarınız başarılı bir şekilde yüklendi:"
#: src/gettext_strings.rs:52
msgid ""
"<strong>Note:</strong> To make keys searchable by email address, you must "
"upload them individually."
@@ -350,27 +271,22 @@ msgstr ""
"<strong>Not:</strong> Anahtarlarınızı e-posta adresiyle bulunabilir yapmak "
"için, her birini ayrı ayrı yüklemeniz gerekiyor."
#: src/gettext_strings.rs:53
msgid "Verifying your email address…"
msgstr "E-posta adresiniz doğrulanıyor..."
#: src/gettext_strings.rs:54
msgid ""
"If the process doesn't complete after a few seconds, please <input "
"type=\"submit\" class=\"textbutton\" value=\"click here\" />."
"If the process doesn't complete after a few seconds, please <input type="
"\"submit\" class=\"textbutton\" value=\"click here\" />."
msgstr ""
"Eğer işlem bir kaç saniye içerisinde tamamlanmazsa, lütfen <input "
"type=\"submit\" class=\"textbutton\" value=\"click here\" />."
"Eğer işlem bir kaç saniye içerisinde tamamlanmazsa, lütfen <input type="
"\"submit\" class=\"textbutton\" value=\"click here\" />."
#: src/gettext_strings.rs:56
msgid "Manage your key on {{domain}}"
msgstr "{{ domain }} üzerindeki anahtarınızı yönetin"
#: src/gettext_strings.rs:58
msgid "Hi,"
msgstr "Merhaba,"
#: src/gettext_strings.rs:59
msgid ""
"This is an automated message from <a href=\"{{base_uri}}\" style=\"text-"
"decoration:none; color: #333\">{{domain}}</a>."
@@ -378,52 +294,41 @@ msgstr ""
"Bu <a href=\"{{base_uri}}\" style=\"text-decoration:none; color: "
"#333\">{{domain}}</a> tarafından gönderilen otomatik bir mesajdır. "
#: src/gettext_strings.rs:60
msgid "If you didn't request this message, please ignore it."
msgstr "Eğer bu mesajı siz talep etmediyseniz, lütfen görmezden gelin."
#: src/gettext_strings.rs:61
msgid "OpenPGP key: <tt>{{primary_fp}}</tt>"
msgstr "OpenPGP anahtarı: <tt>{{primary_fp}}</tt>"
#: src/gettext_strings.rs:62
msgid ""
"To manage and delete listed addresses on this key, please follow the link "
"below:"
msgstr ""
"Bu anahtarla listelenmiş adresleri yönetmek ve silmek için, lütfen aşağıdaki"
" bağlantıya tıklayın:"
"Bu anahtarla listelenmiş adresleri yönetmek ve silmek için, lütfen aşağıdaki "
"bağlantıya tıklayın:"
#: src/gettext_strings.rs:63
msgid ""
"You can find more info at <a "
"href=\"{{base_uri}}/about\">{{domain}}/about</a>."
"You can find more info at <a href=\"{{base_uri}}/about\">{{domain}}/about</"
"a>."
msgstr ""
"Daha fazla bilgiyi <a href=\"{{base_uri}}/about\">{{domain}}/about</a> "
"sayfasında bulabilirsiniz."
#: src/gettext_strings.rs:64
msgid "distributing OpenPGP keys since 2019"
msgstr "OpenPGP anahtarları 2019 yılından beri dağıtılıyor"
#: src/gettext_strings.rs:67
msgid "This is an automated message from {{domain}}."
msgstr "Bu {{domain}} tarafından gönderilen otomatik bir mesajdır. "
#: src/gettext_strings.rs:69
msgid "OpenPGP key: {{primary_fp}}"
msgstr "OpenPGP anahtarı: {{primary_fp}}"
#: src/gettext_strings.rs:71
msgid "You can find more info at {{base_uri}}/about"
msgstr "Daha fazla bilgiyi {{base_uri}}/about sayfasında bulabilirsiniz."
#: src/gettext_strings.rs:74
msgid "Verify {{userid}} for your key on {{domain}}"
msgstr ""
"{{userid}} kimliğini {{domain}} üzerindeki anahtarınız için doğrulayın"
msgstr "{{userid}} kimliğini {{domain}} üzerindeki anahtarınız için doğrulayın"
#: src/gettext_strings.rs:80
msgid ""
"To let others find this key from your email address \"<a rel=\"nofollow\" "
"href=\"#\" style=\"text-decoration:none; color: #333\">{{userid}}</a>\", "
@@ -433,7 +338,6 @@ msgstr ""
"decoration:none; color: #333\">{{userid}}</a>\" e-posta adresinizi "
"kullanarak bulmasını istiyorsanız lütfen aşağıdaki bağlantıya tıklayın:"
#: src/gettext_strings.rs:88
msgid ""
"To let others find this key from your email address \"{{userid}}\",\n"
"please follow the link below:"
@@ -441,42 +345,57 @@ msgstr ""
"Başkalarının bu anahtarı, \"{{userid}}\" e-posta adresinizi kullanarak "
"bulmasını istiyorsanız lütfen aşağıdaki bağlantıya gidin:"
#: src/web/manage.rs:103
msgid "No key found for fingerprint {}"
msgstr "Parmak izi {} için anahtar bulunamadı"
msgid "No key found for key id {}"
msgstr "Anahtar kimliği {} için anahtar bulunamadı"
msgid "No key found for email address {}"
msgstr "E-posta adresi {} için anahtar bulunamadı"
msgid "Search by Short Key ID is not supported."
msgstr "Kısa Anahtar Kimliği ile arama desteklenmiyor."
msgid "Invalid search query."
msgstr "Geçersiz arama sorgusu."
msgctxt "Subject for verification email, {0} = userid, {1} = keyserver domain"
msgid "Verify {0} for your key on {1}"
msgstr "{1} üzerindeki anahtarınız için {0} doğrulaması"
msgctxt "Subject for manage email, {} = keyserver domain"
msgid "Manage your key on {}"
msgstr "{} sunucusundaki anahtarınızı yönetin"
msgid "This link is invalid or expired"
msgstr "Bu bağlantı geçersiz veya süresi geçmiş."
#: src/web/manage.rs:129
msgid "Malformed address: {address}"
msgstr "Hatalı adres: {address}"
#, fuzzy
msgid "Malformed address: {}"
msgstr "Hatalı adres: {}"
#: src/web/manage.rs:136
msgid "No key for address: {address}"
msgstr "Anahtarsız adres: {address}"
#, fuzzy
msgid "No key for address: {}"
msgstr "Anahtarsız adres: {}"
#: src/web/manage.rs:152
msgid "A request has already been sent for this address recently."
msgstr "Bu adres için bir istek kısa bir süre önce zaten gönderilmişti."
#: src/web/vks.rs:111
msgid "Parsing of key data failed."
msgstr "Anahtar verisi çözümlemesi başarısız oldu."
#: src/web/vks.rs:120
msgid "Whoops, please don't upload secret keys!"
msgstr "Eyvah! Lütfen gizli anahtarınızı yüklemeyin!"
#: src/web/vks.rs:133
msgid "No key uploaded."
msgstr "Anahtar yüklenmedi."
#: src/web/vks.rs:177
msgid "Error processing uploaded key."
msgstr "Yüklenen anahtar işlenirken bir hata oluştu."
#: src/web/vks.rs:247
msgid "Upload session expired. Please try again."
msgstr "Yükleme oturumunun süresi doldu. Lütfen tekrar deneyin."
#: src/web/vks.rs:284
msgid "Invalid verification link."
msgstr "Geçersiz doğrulama bağlantısı."

View File

@@ -16,58 +16,21 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#: src/mail.rs:107
msgctxt "Subject for verification email"
msgid "Verify {userid} for your key on {domain}"
msgstr "在 {domain} 上验证你的密钥 {userid}"
#: src/mail.rs:140
msgctxt "Subject for manage email"
msgid "Manage your key on {domain}"
msgstr "在 {domain} 上管理你的密钥"
#: src/i18n_helpers.rs:8
msgid "No key found for fingerprint {fingerprint}"
msgstr ""
#: src/i18n_helpers.rs:10
msgid "No key found for key id {key_id}"
msgstr ""
#: src/i18n_helpers.rs:12
#, fuzzy
msgid "No key found for email address {email}"
msgstr "此地址下没有密钥:{address}"
#: src/i18n_helpers.rs:13
msgid "Search by Short Key ID is not supported."
msgstr ""
#: src/i18n_helpers.rs:14
msgid "Invalid search query."
msgstr ""
#: src/gettext_strings.rs:4
msgid "Error"
msgstr "错误"
#: src/gettext_strings.rs:5
msgid "Looks like something went wrong :("
msgstr "看上去有些东西出错了 :("
#: src/gettext_strings.rs:6
msgid "Error message: {{ internal_error }}"
msgstr "错误信息:{{ internal_error }}"
#: src/gettext_strings.rs:7
msgid "There was an error with your request:"
msgstr "你的请求有一个错误:"
#: src/gettext_strings.rs:8
msgid "We found an entry for <span class=\"email\">{{ query }}</span>:"
msgstr "我们找到了询问的地址<span class=\"email\">{{ query }}</span>"
#: src/gettext_strings.rs:9
msgid ""
"<strong>Hint:</strong> It's more convenient to use <span class=\"brand"
"\">keys.openpgp.org</span> from your OpenPGP software.<br /> Take a look at "
@@ -77,48 +40,38 @@ msgstr ""
"openpgp.org</span>会更方便。<br />查阅我们的<a href=\"/about/usage\">用户指南"
"</a>获取细节。"
#: src/gettext_strings.rs:10
msgid "debug info"
msgstr "调试信息"
#: src/gettext_strings.rs:11
msgid "Search by Email Address / Key ID / Fingerprint"
msgstr "通过电子邮件地址 / 密钥ID / 指纹搜索"
#: src/gettext_strings.rs:12
msgid "Search"
msgstr "搜索"
#: src/gettext_strings.rs:13
msgid ""
"You can also <a href=\"/upload\">upload</a> or <a href=\"/manage\">manage</"
"a> your key."
msgstr ""
"你也可以<a href=\"/upload\">上传</a>或<a href=\"/manage\">管理</a>你的密钥。"
#: src/gettext_strings.rs:14
msgid "Find out more <a href=\"/about\">about this service</a>."
msgstr "找到<a href=\"/about\">关于这项服务</a>的更多信息。"
#: src/gettext_strings.rs:15
msgid "News:"
msgstr "新闻:"
#: src/gettext_strings.rs:16
msgid ""
"<a href=\"/about/news#2019-11-12-celebrating-100k\">Celebrating 100.000 "
"verified addresses! 📈</a> (2019-11-12)"
msgstr ""
#: src/gettext_strings.rs:17
msgid "v{{ version }} built from"
msgstr "v{{ version }} built from"
#: src/gettext_strings.rs:18
msgid "Powered by <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>"
msgstr "由<a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>支持"
#: src/gettext_strings.rs:19
msgid ""
"Background image retrieved from <a href=\"https://www.toptal.com/designers/"
"subtlepatterns/subtle-grey/\">Subtle Patterns</a> under CC BY-SA 3.0"
@@ -126,23 +79,18 @@ msgstr ""
"背景图从<a href=\"https://www.toptal.com/designers/subtlepatterns/subtle-"
"grey/\">Subtle Patterns</a>依据CC BY-SA 3.0协议获得"
#: src/gettext_strings.rs:20
msgid "Maintenance Mode"
msgstr "修复模式"
#: src/gettext_strings.rs:21
msgid "Manage your key"
msgstr "管理你的密钥"
#: src/gettext_strings.rs:22
msgid "Enter any verified email address for your key"
msgstr "输入任意关联到你的密钥的电子邮件地址"
#: src/gettext_strings.rs:23
msgid "Send link"
msgstr "发送链接"
#: src/gettext_strings.rs:24
msgid ""
"We will send you an email with a link you can use to remove any of your "
"email addresses from search."
@@ -150,7 +98,6 @@ msgstr ""
"我们将把链接用电子邮件的形式发给你,那条链接可以让你在搜索结果中移除你的任意"
"邮箱地址"
#: src/gettext_strings.rs:25
msgid ""
"Managing the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
@@ -158,15 +105,12 @@ msgstr ""
"管理密钥<span class=\"fingerprint\"><a href=\"{{ key_link }}\" target="
"\"_blank\">{{ key_fpr }}</a></span>"
#: src/gettext_strings.rs:26
msgid "Your key is published with the following identity information:"
msgstr "你的密钥已经用以下身份发布:"
#: src/gettext_strings.rs:27
msgid "Delete"
msgstr "删除"
#: src/gettext_strings.rs:28
msgid ""
"Clicking \"delete\" on any address will remove it from this key. It will no "
"longer appear in a search.<br /> To add another address, <a href=\"/upload"
@@ -175,7 +119,6 @@ msgstr ""
"点击“删除”将会在密钥中删除该地址的关联。之后该地址将不会在搜索结果中出现。"
"<br />如果需要添加另外的地址,请<a href=\"/upload\">重新上传此密钥</a>"
#: src/gettext_strings.rs:29
msgid ""
"Your key is published as only non-identity information. (<a href=\"/about\" "
"target=\"_blank\">What does this mean?</a>)"
@@ -183,11 +126,9 @@ msgstr ""
"你的密钥以无身份的形式发布。(<a href=\"/about\" target=\"_blank\">这意味着什"
"么?</a>"
#: src/gettext_strings.rs:30
msgid "To add an address, <a href=\"/upload\">upload</a> the key again."
msgstr "如果要添加地址,<a href=\"/upload\">重新上传此密钥</a>"
#: src/gettext_strings.rs:31
msgid ""
"We have sent an email with further instructions to <span class=\"email"
"\">{{ address }}</span>."
@@ -195,11 +136,9 @@ msgstr ""
"我们已经向<span class=\"email\">{{ address }}</span>发送了包含后续说明的电子"
"邮件。"
#: src/gettext_strings.rs:32
msgid "This address has already been verified."
msgstr "该地址已经被验证过了。"
#: src/gettext_strings.rs:33
msgid ""
"Your key <span class=\"fingerprint\">{{ key_fpr }}</span> is now published "
"for the identity <a href=\"{{userid_link}}\" target=\"_blank\"><span class="
@@ -209,15 +148,12 @@ msgstr ""
"\"{{userid_link}}\" target=\"_blank\"><span class=\"email\">{{ userid }}</"
"span></a>的身份发布"
#: src/gettext_strings.rs:34
msgid "Upload your key"
msgstr "上传你的密钥"
#: src/gettext_strings.rs:35
msgid "Upload"
msgstr "上传"
#: src/gettext_strings.rs:36
msgid ""
"Need more info? Check our <a target=\"_blank\" href=\"/about\">intro</a> and "
"<a target=\"_blank\" href=\"/about/usage\">usage guide</a>."
@@ -225,7 +161,6 @@ msgstr ""
"想了解更多信息?查阅我们的<a target=\"_blank\" href=\"/about\">介绍</a>和<a "
"target=\"_blank\" href=\"/about/usage\">用户指南</a>。"
#: src/gettext_strings.rs:37
msgid ""
"You uploaded the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
@@ -233,11 +168,9 @@ msgstr ""
"你上传了密钥<span class=\"fingerprint\"><a href=\"{{ key_link }}\" target="
"\"_blank\">{{ key_fpr }}</a></span>。"
#: src/gettext_strings.rs:38
msgid "This key is revoked."
msgstr "该密钥已经被吊销。"
#: src/gettext_strings.rs:39
msgid ""
"It is published without identity information and can't be made available for "
"search by email address (<a href=\"/about\" target=\"_blank\">what does this "
@@ -246,7 +179,6 @@ msgstr ""
"该密钥以无身份信息的形式发布,不能使用电子邮件地址搜索到(<a href=\"/about\" "
"target=\"_blank\">这意味着什么?</a>"
#: src/gettext_strings.rs:40
msgid ""
"This key is now published with the following identity information (<a href="
"\"/about\" target=\"_blank\">what does this mean?</a>):"
@@ -254,11 +186,9 @@ msgstr ""
"该密钥已经用以下身份信息发布(<a href=\"/about\" target=\"_blank\">这意味着什"
"么?</a>"
#: src/gettext_strings.rs:41
msgid "Published"
msgstr "已发布"
#: src/gettext_strings.rs:42
msgid ""
"This key is now published with only non-identity information. (<a href=\"/"
"about\" target=\"_blank\">What does this mean?</a>)"
@@ -266,17 +196,14 @@ msgstr ""
"你的密钥现在以无身份的形式发布。(<a href=\"/about\" target=\"_blank\">这意味"
"着什么?</a>"
#: src/gettext_strings.rs:43
msgid ""
"To make the key available for search by email address, you can verify it "
"belongs to you:"
msgstr "要让密钥能通过电子邮件地址搜索到,你可以验证该地址属于你:"
#: src/gettext_strings.rs:44
msgid "Verification Pending"
msgstr "等待验证"
#: src/gettext_strings.rs:45
msgid ""
"<strong>Note:</strong> Some providers delay emails for up to 15 minutes to "
"prevent spam. Please be patient."
@@ -284,11 +211,9 @@ msgstr ""
"<strong>提醒:</strong>部分服务商会将邮件拖延最多15分钟来阻止垃圾邮件。请保持"
"耐心。"
#: src/gettext_strings.rs:46
msgid "Send Verification Email"
msgstr "发送验证邮件"
#: src/gettext_strings.rs:47
msgid ""
"This key contains one identity that could not be parsed as an email address."
"<br /> This identity can't be published on <span class=\"brand\">keys."
@@ -299,7 +224,6 @@ msgstr ""
"\"brand\">keys.openpgp.org</span>上发布。 <a href=\"/about/faq#non-email-"
"uids\" target=\"_blank\">为什么?</a>"
#: src/gettext_strings.rs:48
msgid ""
"This key contains {{ count_unparsed }} identities that could not be parsed "
"as an email address.<br /> These identities can't be published on <span "
@@ -310,7 +234,6 @@ msgstr ""
"无法在<span class=\"brand\">keys.openpgp.org</span>上发布。 <a href=\"/"
"about/faq#non-email-uids\" target=\"_blank\">为什么?</a>"
#: src/gettext_strings.rs:49
msgid ""
"This key contains one revoked identity, which is not published. (<a href=\"/"
"about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
@@ -318,7 +241,6 @@ msgstr ""
"该密钥包含一个被吊销的身份,所以没有发布(<a href=\"/about/faq#revoked-uids"
"\" target=\"_blank\">为什么?</a>"
#: src/gettext_strings.rs:50
msgid ""
"This key contains {{ count_revoked }} revoked identities, which are not "
"published. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
@@ -326,11 +248,9 @@ msgstr ""
"该密钥包含 {{ count_revoked }}个被吊销的身份,所以没有发布(<a href=\"/about/"
"faq#revoked-uids\" target=\"_blank\">为什么?</a>"
#: src/gettext_strings.rs:51
msgid "Your keys have been successfully uploaded:"
msgstr "你的密钥已成功上传"
#: src/gettext_strings.rs:52
msgid ""
"<strong>Note:</strong> To make keys searchable by email address, you must "
"upload them individually."
@@ -338,11 +258,9 @@ msgstr ""
"<strong>提示:</strong>如果想让一些密钥可以通过邮箱地址搜索到,你必须把这些密"
"钥逐个上传。"
#: src/gettext_strings.rs:53
msgid "Verifying your email address…"
msgstr "正在验证你的邮箱地址……"
#: src/gettext_strings.rs:54
msgid ""
"If the process doesn't complete after a few seconds, please <input type="
"\"submit\" class=\"textbutton\" value=\"click here\" />."
@@ -350,15 +268,12 @@ msgstr ""
"如果这项操作在若干秒后仍没有完成,<input type=\"submit\" class=\"textbutton"
"\" value=\"click here\" />。"
#: src/gettext_strings.rs:56
msgid "Manage your key on {{domain}}"
msgstr "在{{domain}}上管理你的密钥"
#: src/gettext_strings.rs:58
msgid "Hi,"
msgstr "你好,"
#: src/gettext_strings.rs:59
msgid ""
"This is an automated message from <a href=\"{{base_uri}}\" style=\"text-"
"decoration:none; color: #333\">{{domain}}</a>."
@@ -366,21 +281,17 @@ msgstr ""
"这是来自<a href=\"{{base_uri}}\" style=\"text-decoration:none; color: "
"#333\">{{domain}}</a>的一条自动信息"
#: src/gettext_strings.rs:60
msgid "If you didn't request this message, please ignore it."
msgstr "如果你没有申请过这条信息,请忽略。"
#: src/gettext_strings.rs:61
msgid "OpenPGP key: <tt>{{primary_fp}}</tt>"
msgstr "OpenPGP密钥<tt>{{primary_fp}}</tt>"
#: src/gettext_strings.rs:62
msgid ""
"To manage and delete listed addresses on this key, please follow the link "
"below:"
msgstr "要管理和删除此密钥上列出的地址,请点击以下链接:"
#: src/gettext_strings.rs:63
msgid ""
"You can find more info at <a href=\"{{base_uri}}/about\">{{domain}}/about</"
"a>."
@@ -388,27 +299,21 @@ msgstr ""
"你可以在以下链接找到更多信息 <a href=\"{{base_uri}}/about\">{{domain}}/"
"about</a>。"
#: src/gettext_strings.rs:64
msgid "distributing OpenPGP keys since 2019"
msgstr "自2019年起传播 OpenPGP 密钥"
#: src/gettext_strings.rs:67
msgid "This is an automated message from {{domain}}."
msgstr "这是一条来自 {{domain}} 的自动信息。"
#: src/gettext_strings.rs:69
msgid "OpenPGP key: {{primary_fp}}"
msgstr "OpenPGP密钥{{primary_fp}}"
#: src/gettext_strings.rs:71
msgid "You can find more info at {{base_uri}}/about"
msgstr "你可以在以下链接找到更多信息 {{base_uri}}/about"
#: src/gettext_strings.rs:74
msgid "Verify {{userid}} for your key on {{domain}}"
msgstr "在 {{domain}} 上验证你的密钥 {{userid}}"
#: src/gettext_strings.rs:80
msgid ""
"To let others find this key from your email address \"<a rel=\"nofollow\" "
"href=\"#\" style=\"text-decoration:none; color: #333\">{{userid}}</a>\", "
@@ -417,7 +322,6 @@ msgstr ""
"要让其他人能通过你的邮件地址找到此密钥\"<a rel=\"nofollow\" href=\"#\" style="
"\"text-decoration:none; color: #333\">{{userid}}</a>\",请点击下方的链接:"
#: src/gettext_strings.rs:88
msgid ""
"To let others find this key from your email address \"{{userid}}\",\n"
"please follow the link below:"
@@ -425,43 +329,58 @@ msgstr ""
"要让其他人能通过你的邮件地址找到此密钥\"{{userid}}\"\n"
"请点击下方的链接:"
#: src/web/manage.rs:103
msgid "No key found for fingerprint {}"
msgstr "此地址下没有密钥:{}"
msgid "No key found for key id {}"
msgstr "此地址下没有密钥:{}"
msgid "No key found for email address {}"
msgstr "此地址下没有密钥:{}"
msgid "Search by Short Key ID is not supported."
msgstr ""
msgid "Invalid search query."
msgstr ""
msgctxt "Subject for verification email, {0} = userid, {1} = keyserver domain"
msgid "Verify {0} for your key on {1}"
msgstr "在 {0} 上验证你的密钥 {1}"
msgctxt "Subject for manage email, {} = keyserver domain"
msgid "Manage your key on {}"
msgstr "在 {} 上管理你的密钥"
msgid "This link is invalid or expired"
msgstr "此链接无效或者已经过期"
#: src/web/manage.rs:129
msgid "Malformed address: {address}"
msgstr "地址格式错误:{address}"
#, fuzzy
msgid "Malformed address: {}"
msgstr "地址格式错误:{}"
#: src/web/manage.rs:136
msgid "No key for address: {address}"
msgstr "此地址下没有密钥:{address}"
#, fuzzy
msgid "No key for address: {}"
msgstr "此地址下没有密钥:{}"
#: src/web/manage.rs:152
msgid "A request has already been sent for this address recently."
msgstr "对此地址的申请已经发送"
#: src/web/vks.rs:111
msgid "Parsing of key data failed."
msgstr "密钥数据解析失败。"
#: src/web/vks.rs:120
msgid "Whoops, please don't upload secret keys!"
msgstr "我的天!请不要上传私钥!"
#: src/web/vks.rs:133
msgid "No key uploaded."
msgstr "没有密钥被上传"
#: src/web/vks.rs:177
msgid "Error processing uploaded key."
msgstr "处理已上传的密钥时出现问题"
#: src/web/vks.rs:247
msgid "Upload session expired. Please try again."
msgstr "上传会话过期,请重试。"
#: src/web/vks.rs:284
msgid "Invalid verification link."
msgstr "无效的验证链接"

View File

@@ -1 +0,0 @@
nightly-2020-06-01

3
rust-toolchain.toml Normal file
View File

@@ -0,0 +1,3 @@
[toolchain]
channel = "1.86"
components = ["rustfmt", "clippy"]

View File

@@ -1,6 +0,0 @@
overflow_delimited_expr = true
unstable_features = true
use_small_heuristics = "Max"
fn_args_density = "Compressed"
max_width = 80
force_multiline_blocks = true

View File

@@ -1,25 +1,27 @@
with import <nixpkgs> {};
let
src = fetchFromGitHub {
owner = "mozilla";
repo = "nixpkgs-mozilla";
# commit from: 2019-07-15
rev = "8c007b60731c07dd7a052cce508de3bb1ae849b4";
sha256 = "sha256-RsNPnEKd7BcogwkqhaV5kI/HuNC4flH/OQCC/4W5y/8=";
};
rustOverlay = import "${src.out}/rust-overlay.nix" pkgs pkgs;
rustChannel = (rustOverlay.rustChannelOf { rustToolchain = ./rust-toolchain; });
oxalica_overlay = import (builtins.fetchTarball
"https://github.com/oxalica/rust-overlay/archive/master.tar.gz");
pkgs = import <nixpkgs> { overlays = [ oxalica_overlay ]; };
rust_channel = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;
#rust_channel = pkgs.rust-bin.stable.latest.default;
in
stdenv.mkDerivation {
name = "rust-env";
buildInputs = [
rustChannel.rust
# latest.rustChannels.nightly.rust
# latest.rustChannels.stable.rust
pkgs.mkShell {
nativeBuildInputs = [
(rust_channel.override {
extensions = [ "rust-src" "rust-std" "clippy" ];
targets = [
"x86_64-unknown-linux-gnu"
];
})
];
buildInputs = with pkgs; [
sqlite
openssl
clang
nettle
pkgconfig
pkg-config
gettext
transifex-client
@@ -30,6 +32,7 @@ stdenv.mkDerivation {
# compilation of -sys packages requires manually setting this :(
shellHook = ''
export LIBCLANG_PATH="${pkgs.llvmPackages.libclang}/lib";
export LIBCLANG_PATH="${pkgs.llvmPackages.libclang.lib}/lib";
'';
}

View File

@@ -1,71 +1,191 @@
use lazy_static::lazy_static;
use hagrid_database::types::Email;
use std::collections::HashSet;
use crate::database::types::Email;
use std::sync::LazyLock;
// from https://github.com/mailcheck/mailcheck/wiki/List-of-Popular-Domains
lazy_static! {
static ref POPULAR_DOMAINS: HashSet<&'static str> = vec!(
static POPULAR_DOMAINS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
vec![
/* Default domains included */
"aol.com", "att.net", "comcast.net", "facebook.com", "gmail.com", "gmx.com", "googlemail.com",
"google.com", "hotmail.com", "hotmail.co.uk", "mac.com", "me.com", "mail.com", "msn.com",
"live.com", "sbcglobal.net", "verizon.net", "yahoo.com", "yahoo.co.uk",
"aol.com",
"att.net",
"comcast.net",
"facebook.com",
"gmail.com",
"gmx.com",
"googlemail.com",
"google.com",
"hotmail.com",
"hotmail.co.uk",
"mac.com",
"me.com",
"mail.com",
"msn.com",
"live.com",
"sbcglobal.net",
"verizon.net",
"yahoo.com",
"yahoo.co.uk",
/* Other global domains */
"email.com", "fastmail.fm", "games.com" /* AOL */, "gmx.net", "hush.com", "hushmail.com", "icloud.com",
"iname.com", "inbox.com", "lavabit.com", "love.com" /* AOL */, "mailbox.org", "posteo.de", "outlook.com", "pobox.com", "protonmail.ch", "protonmail.com", "tutanota.de", "tutanota.com", "tutamail.com", "tuta.io",
"keemail.me", "rocketmail.com" /* Yahoo */, "safe-mail.net", "wow.com" /* AOL */, "ygm.com" /* AOL */,
"ymail.com" /* Yahoo */, "zoho.com", "yandex.com",
"email.com",
"fastmail.fm",
"games.com", /* AOL */
"gmx.net",
"hush.com",
"hushmail.com",
"icloud.com",
"iname.com",
"inbox.com",
"lavabit.com",
"love.com", /* AOL */
"mailbox.org",
"posteo.de",
"outlook.com",
"pobox.com",
"protonmail.ch",
"protonmail.com",
"tutanota.de",
"tutanota.com",
"tutamail.com",
"tuta.io",
"keemail.me",
"rocketmail.com", /* Yahoo */
"safe-mail.net",
"wow.com", /* AOL */
"ygm.com", /* AOL */
"ymail.com", /* Yahoo */
"zoho.com",
"yandex.com",
/* United States ISP domains */
"bellsouth.net", "charter.net", "cox.net", "earthlink.net", "juno.com",
"bellsouth.net",
"charter.net",
"cox.net",
"earthlink.net",
"juno.com",
/* British ISP domains */
"btinternet.com", "virginmedia.com", "blueyonder.co.uk", "freeserve.co.uk", "live.co.uk",
"ntlworld.com", "o2.co.uk", "orange.net", "sky.com", "talktalk.co.uk", "tiscali.co.uk",
"virgin.net", "wanadoo.co.uk", "bt.com",
"btinternet.com",
"virginmedia.com",
"blueyonder.co.uk",
"freeserve.co.uk",
"live.co.uk",
"ntlworld.com",
"o2.co.uk",
"orange.net",
"sky.com",
"talktalk.co.uk",
"tiscali.co.uk",
"virgin.net",
"wanadoo.co.uk",
"bt.com",
/* Domains used in Asia */
"sina.com", "sina.cn", "qq.com", "naver.com", "hanmail.net", "daum.net", "nate.com", "yahoo.co.jp", "yahoo.co.kr", "yahoo.co.id", "yahoo.co.in", "yahoo.com.sg", "yahoo.com.ph", "163.com", "yeah.net", "126.com", "21cn.com", "aliyun.com", "foxmail.com",
"sina.com",
"sina.cn",
"qq.com",
"naver.com",
"hanmail.net",
"daum.net",
"nate.com",
"yahoo.co.jp",
"yahoo.co.kr",
"yahoo.co.id",
"yahoo.co.in",
"yahoo.com.sg",
"yahoo.com.ph",
"163.com",
"yeah.net",
"126.com",
"21cn.com",
"aliyun.com",
"foxmail.com",
/* French ISP domains */
"hotmail.fr", "live.fr", "laposte.net", "yahoo.fr", "wanadoo.fr", "orange.fr", "gmx.fr", "sfr.fr", "neuf.fr", "free.fr",
"hotmail.fr",
"live.fr",
"laposte.net",
"yahoo.fr",
"wanadoo.fr",
"orange.fr",
"gmx.fr",
"sfr.fr",
"neuf.fr",
"free.fr",
/* German ISP domains */
"gmx.de", "hotmail.de", "live.de", "online.de", "t-online.de" /* T-Mobile */, "web.de", "yahoo.de",
"gmx.de",
"hotmail.de",
"live.de",
"online.de",
"t-online.de", /* T-Mobile */
"web.de",
"yahoo.de",
/* Italian ISP domains */
"libero.it", "virgilio.it", "hotmail.it", "aol.it", "tiscali.it", "alice.it", "live.it", "yahoo.it", "email.it", "tin.it", "poste.it", "teletu.it",
"libero.it",
"virgilio.it",
"hotmail.it",
"aol.it",
"tiscali.it",
"alice.it",
"live.it",
"yahoo.it",
"email.it",
"tin.it",
"poste.it",
"teletu.it",
/* Russian ISP domains */
"mail.ru", "rambler.ru", "yandex.ru", "ya.ru", "list.ru",
"mail.ru",
"rambler.ru",
"yandex.ru",
"ya.ru",
"list.ru",
/* Belgian ISP domains */
"hotmail.be", "live.be", "skynet.be", "voo.be", "tvcablenet.be", "telenet.be",
"hotmail.be",
"live.be",
"skynet.be",
"voo.be",
"tvcablenet.be",
"telenet.be",
/* Argentinian ISP domains */
"hotmail.com.ar", "live.com.ar", "yahoo.com.ar", "fibertel.com.ar", "speedy.com.ar", "arnet.com.ar",
"hotmail.com.ar",
"live.com.ar",
"yahoo.com.ar",
"fibertel.com.ar",
"speedy.com.ar",
"arnet.com.ar",
/* Domains used in Mexico */
"yahoo.com.mx", "live.com.mx", "hotmail.es", "hotmail.com.mx", "prodigy.net.mx",
"yahoo.com.mx",
"live.com.mx",
"hotmail.es",
"hotmail.com.mx",
"prodigy.net.mx",
/* Domains used in Brazil */
"yahoo.com.br", "hotmail.com.br", "outlook.com.br", "uol.com.br", "bol.com.br", "terra.com.br", "ig.com.br", "itelefonica.com.br", "r7.com", "zipmail.com.br", "globo.com", "globomail.com", "oi.com.br"
).into_iter().collect();
}
"yahoo.com.br",
"hotmail.com.br",
"outlook.com.br",
"uol.com.br",
"bol.com.br",
"terra.com.br",
"ig.com.br",
"itelefonica.com.br",
"r7.com",
"zipmail.com.br",
"globo.com",
"globomail.com",
"oi.com.br",
]
.into_iter()
.collect()
});
pub fn anonymize_address(email: &Email) -> Option<String> {
email.as_str()
.rsplit('@')
.next()
.map(|domain| domain.to_lowercase())
.and_then(|domain| {
if POPULAR_DOMAINS.contains(&domain.as_str()) {
Some(domain)
} else {
domain.rsplit('.').next().map(|tld| tld.to_owned())
}
})
email
.as_str()
.rsplit('@')
.next()
.map(|domain| domain.to_lowercase())
.and_then(|domain| {
if POPULAR_DOMAINS.contains(&domain.as_str()) {
Some(domain)
} else {
domain.rsplit('.').next().map(|tld| tld.to_owned())
}
})
}
pub fn anonymize_address_fallback(email: &Email) -> String {

64
src/app/config.rs Normal file
View File

@@ -0,0 +1,64 @@
use rocket::figment::Figment;
use rocket::serde::Deserialize;
use std::path::PathBuf;
pub const DEFAULT_MAIL_RATE_LIMIT: u64 = 60; // in seconds
pub const DEFAULT_MAINTENANCE_FILE_NAME: &str = "maintenance";
pub const DEFAULT_LOCALIZED_TEMPLATE_DIR_NAME: &str = "localized";
pub fn load(figment: &Figment) -> Configuration {
figment
.extract::<Configuration>()
.expect("Rocket config must succeed")
}
#[derive(Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct Configuration {
// database
pub keys_internal_dir: PathBuf,
// hagrid state
pub assets_dir: PathBuf,
// state
#[serde(rename = "base-URI")]
pub base_uri: String,
#[serde(rename = "base-URI-Onion")]
pub base_uri_onion: Option<String>,
// stateful_token_service
pub token_dir: PathBuf,
// stateless_token_service
pub token_secret: String,
pub token_validity: u64,
// mail_service
pub email_template_dir: PathBuf,
pub from: String,
pub filemail_into: Option<PathBuf>,
pub local_smtp: Option<bool>,
// rate_limiter
#[serde(default = "Configuration::default_mail_rate_limit")]
pub mail_rate_limit: u64,
// maintenance
#[serde(default = "Configuration::default_maintenance_file")]
pub maintenance_file: PathBuf,
// localized_template_list
pub template_dir: PathBuf,
// prometheus
#[serde(default)]
pub enable_prometheus: bool,
}
impl Configuration {
pub fn base_uri_onion(&self) -> &str {
self.base_uri_onion
.as_deref()
.unwrap_or_else(|| &self.base_uri)
}
pub fn default_mail_rate_limit() -> u64 {
DEFAULT_MAIL_RATE_LIMIT
}
pub fn default_maintenance_file() -> PathBuf {
PathBuf::from(DEFAULT_MAINTENANCE_FILE_NAME)
}
}

26
src/app/mod.rs Normal file
View File

@@ -0,0 +1,26 @@
pub mod config;
pub mod state;
use crate::initializers;
use rocket::Build;
use rocket::Rocket;
pub fn run() {
let rocket = configure_rocket(rocket::build());
run_rocket(rocket).unwrap_or_else(|e| eprintln!("Hagrid Error: {e}"))
}
pub fn configure_rocket(rocket: Rocket<Build>) -> Rocket<Build> {
let config = config::load(rocket.figment());
initializers::run(rocket, &config).expect("Initializers error")
}
pub fn run_rocket(rocket: Rocket<Build>) -> Result<(), rocket::Error> {
::rocket::async_main(async move {
let _rocket = rocket.ignite().await?.launch().await?;
Ok(())
})
}

10
src/app/state.rs Normal file
View File

@@ -0,0 +1,10 @@
use std::path::PathBuf;
pub struct ApplicationState {
/// Assets directory, mounted to /assets, served by hagrid or nginx
pub assets_dir: PathBuf,
/// XXX
pub base_uri: String,
pub base_uri_onion: String,
}

View File

@@ -1,22 +1,36 @@
use lazy_static::lazy_static;
use rocket_prometheus::prometheus;
use std::sync::LazyLock;
use crate::anonymize_utils;
use crate::database::types::Email;
use hagrid_database::types::Email;
lazy_static! {
static ref KEY_UPLOAD: LabelCounter =
LabelCounter::new("hagrid_key_upload", "Uploaded keys", &["result"]);
static KEY_UPLOAD: LazyLock<LabelCounter> =
LazyLock::new(|| LabelCounter::new("hagrid_key_upload", "Uploaded keys", &["result"]));
static ref MAIL_SENT: LabelCounter =
LabelCounter::new("hagrid_mail_sent", "Sent verification mails", &["type", "domain"]);
static MAIL_SENT: LazyLock<LabelCounter> = LazyLock::new(|| {
LabelCounter::new(
"hagrid_mail_sent",
"Sent verification mails",
&["type", "domain"],
)
});
static ref KEY_ADDRESS_PUBLISHED: LabelCounter =
LabelCounter::new("hagrid_key_address_published", "Verified email addresses", &["domain"]);
static ref KEY_ADDRESS_UNPUBLISHED: LabelCounter =
LabelCounter::new("hagrid_key_address_unpublished", "Unpublished email addresses", &["domain"]);
}
static KEY_ADDRESS_PUBLISHED: LazyLock<LabelCounter> = LazyLock::new(|| {
LabelCounter::new(
"hagrid_key_address_published",
"Verified email addresses",
&["domain"],
)
});
static KEY_ADDRESS_UNPUBLISHED: LazyLock<LabelCounter> = LazyLock::new(|| {
LabelCounter::new(
"hagrid_key_address_unpublished",
"Unpublished email addresses",
&["domain"],
)
});
pub fn register_counters(registry: &prometheus::Registry) {
KEY_UPLOAD.register(registry);
@@ -58,7 +72,9 @@ impl LabelCounter {
}
fn register(&self, registry: &prometheus::Registry) {
registry.register(Box::new(self.prometheus_counter.clone())).unwrap();
registry
.register(Box::new(self.prometheus_counter.clone()))
.unwrap();
}
fn inc(&self, values: &[&str]) {

View File

@@ -1,129 +0,0 @@
//! Deletes (address, key)-binding(s), and/or a key(s).
use std::convert::TryInto;
use std::path::PathBuf;
extern crate anyhow;
use anyhow::Result as Result;
extern crate structopt;
use structopt::StructOpt;
extern crate hagrid_database as database;
use crate::database::{Query, Database, KeyDatabase};
#[derive(Debug, StructOpt)]
#[structopt(
name = "hagrid-delete",
about = "Deletes (address, key)-binding(s), and/or a key(s)."
)]
pub struct Opt {
/// Base directory.
#[structopt(parse(from_os_str))]
base: PathBuf,
/// E-Mail address, Fingerprint, or KeyID of the TPK to delete.
/// If a Fingerprint or KeyID is given, --all is implied.
query: String,
/// Also delete all bindings.
#[structopt(long = "all-bindings")]
all_bindings: bool,
/// Also delete all bindings and the key.
#[structopt(long = "all")]
all: bool,
}
fn main() {
if let Err(e) = real_main() {
eprint!("{}", e);
let mut cause = e.source();
while let Some(c) = cause {
eprint!(":\n {}", c);
cause = c.source();
}
eprintln!();
::std::process::exit(2);
}
}
fn real_main() -> Result<()> {
let opt = Opt::from_args();
let db = KeyDatabase::new_from_base(opt.base.canonicalize()?)?;
delete(&db, &opt.query.parse()?, opt.all_bindings, opt.all)
}
fn delete(db: &KeyDatabase, query: &Query, all_bindings: bool, mut all: bool)
-> Result<()> {
match query {
Query::ByFingerprint(_) | Query::ByKeyID(_) => {
eprintln!("Fingerprint or KeyID given, deleting key and all \
bindings.");
all = true;
},
_ => (),
}
let tpk = db.lookup(&query)?.ok_or_else(
|| anyhow::format_err!("No TPK matching {:?}", query))?;
let fp: database::types::Fingerprint = tpk.fingerprint().try_into()?;
let mut results = Vec::new();
// First, delete the bindings.
if all_bindings || all {
results.push(
("all bindings".into(),
db.set_email_unpublished_all(&fp)));
} else {
if let Query::ByEmail(ref email) = query {
results.push(
(email.to_string(),
db.set_email_unpublished(&fp, email)));
} else {
unreachable!()
}
}
// Now delete the key(s) itself.
if all {
// TODO
/*for skb in tpk.subkeys() {
results.push(
(skb.subkey().fingerprint().to_keyid().to_string(),
db.unlink_kid(&skb.subkey().fingerprint().try_into()?,
&fp)));
results.push(
(skb.subkey().fingerprint().to_string(),
db.unlink_fpr(&skb.subkey().fingerprint().try_into()?,
&fp)));
}
results.push(
(tpk.fingerprint().to_keyid().to_string(),
db.unlink_kid(&tpk.fingerprint().try_into()?,
&fp)));
results.push(
(tpk.fingerprint().to_string(),
db.update(&fp, None)));
*/
}
let mut err = Ok(());
for (slug, result) in results {
eprintln!("{}: {}", slug,
if let Err(ref e) = result {
e.to_string()
} else {
"Deleted".into()
});
if err.is_ok() {
if let Err(e) = result {
err = Err(e);
}
}
}
err
}

File diff suppressed because it is too large Load Diff

View File

@@ -6,57 +6,101 @@ fn _dummy() {
t!("Error message: {{ internal_error }}");
t!("There was an error with your request:");
t!("We found an entry for <span class=\"email\">{{ query }}</span>:");
t!("<strong>Hint:</strong> It's more convenient to use <span class=\"brand\">keys.openpgp.org</span> from your OpenPGP software.<br /> Take a look at our <a href=\"/about/usage\">usage guide</a> for details.");
t!(
"<strong>Hint:</strong> It's more convenient to use <span class=\"brand\">keys.openpgp.org</span> from your OpenPGP software.<br /> Take a look at our <a href=\"/about/usage\">usage guide</a> for details."
);
t!("debug info");
t!("Search by Email Address / Key ID / Fingerprint");
t!("Search");
t!("You can also <a href=\"/upload\">upload</a> or <a href=\"/manage\">manage</a> your key.");
t!("Find out more <a href=\"/about\">about this service</a>.");
t!("News:");
t!("<a href=\"/about/news#2019-11-12-celebrating-100k\">Celebrating 100.000 verified addresses! 📈</a> (2019-11-12)");
t!(
"<a href=\"/about/news#2019-11-12-celebrating-100k\">Celebrating 100.000 verified addresses! 📈</a> (2019-11-12)"
);
t!("v{{ version }} built from");
t!("Powered by <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>");
t!("Background image retrieved from <a href=\"https://www.toptal.com/designers/subtlepatterns/subtle-grey/\">Subtle Patterns</a> under CC BY-SA 3.0");
t!(
"Background image retrieved from <a href=\"https://www.toptal.com/designers/subtlepatterns/subtle-grey/\">Subtle Patterns</a> under CC BY-SA 3.0"
);
t!("Maintenance Mode");
t!("Manage your key");
t!("Enter any verified email address for your key");
t!("Send link");
t!("We will send you an email with a link you can use to remove any of your email addresses from search.");
t!("Managing the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" target=\"_blank\">{{ key_fpr }}</a></span>.");
t!(
"We will send you an email with a link you can use to remove any of your email addresses from search."
);
t!(
"Managing the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" target=\"_blank\">{{ key_fpr }}</a></span>."
);
t!("Your key is published with the following identity information:");
t!("Delete");
t!("Clicking \"delete\" on any address will remove it from this key. It will no longer appear in a search.<br /> To add another address, <a href=\"/upload\">upload</a> the key again.");
t!("Your key is published as only non-identity information. (<a href=\"/about\" target=\"_blank\">What does this mean?</a>)");
t!(
"Clicking \"delete\" on any address will remove it from this key. It will no longer appear in a search.<br /> To add another address, <a href=\"/upload\">upload</a> the key again."
);
t!(
"Your key is published as only non-identity information. (<a href=\"/about\" target=\"_blank\">What does this mean?</a>)"
);
t!("To add an address, <a href=\"/upload\">upload</a> the key again.");
t!("We have sent an email with further instructions to <span class=\"email\">{{ address }}</span>.");
t!(
"We have sent an email with further instructions to <span class=\"email\">{{ address }}</span>."
);
t!("This address has already been verified.");
t!("Your key <span class=\"fingerprint\">{{ key_fpr }}</span> is now published for the identity <a href=\"{{userid_link}}\" target=\"_blank\"><span class=\"email\">{{ userid }}</span></a>.");
t!(
"Your key <span class=\"fingerprint\">{{ key_fpr }}</span> is now published for the identity <a href=\"{{userid_link}}\" target=\"_blank\"><span class=\"email\">{{ userid }}</span></a>."
);
t!("Upload your key");
t!("Upload");
t!("Need more info? Check our <a target=\"_blank\" href=\"/about\">intro</a> and <a target=\"_blank\" href=\"/about/usage\">usage guide</a>.");
t!("You uploaded the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" target=\"_blank\">{{ key_fpr }}</a></span>.");
t!(
"Need more info? Check our <a target=\"_blank\" href=\"/about\">intro</a> and <a target=\"_blank\" href=\"/about/usage\">usage guide</a>."
);
t!(
"You uploaded the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" target=\"_blank\">{{ key_fpr }}</a></span>."
);
t!("This key is revoked.");
t!("It is published without identity information and can't be made available for search by email address (<a href=\"/about\" target=\"_blank\">what does this mean?</a>).");
t!("This key is now published with the following identity information (<a href=\"/about\" target=\"_blank\">what does this mean?</a>):");
t!(
"It is published without identity information and can't be made available for search by email address (<a href=\"/about\" target=\"_blank\">what does this mean?</a>)."
);
t!(
"This key is now published with the following identity information (<a href=\"/about\" target=\"_blank\">what does this mean?</a>):"
);
t!("Published");
t!("This key is now published with only non-identity information. (<a href=\"/about\" target=\"_blank\">What does this mean?</a>)");
t!(
"This key is now published with only non-identity information. (<a href=\"/about\" target=\"_blank\">What does this mean?</a>)"
);
t!("To make the key available for search by email address, you can verify it belongs to you:");
t!("Verification Pending");
t!("<strong>Note:</strong> Some providers delay emails for up to 15 minutes to prevent spam. Please be patient.");
t!(
"<strong>Note:</strong> Some providers delay emails for up to 15 minutes to prevent spam. Please be patient."
);
t!("Send Verification Email");
t!("This key contains one identity that could not be parsed as an email address.<br /> This identity can't be published on <span class=\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-email-uids\" target=\"_blank\">Why?</a>)");
t!("This key contains {{ count_unparsed }} identities that could not be parsed as an email address.<br /> These identities can't be published on <span class=\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-email-uids\" target=\"_blank\">Why?</a>)");
t!("This key contains one revoked identity, which is not published. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)");
t!("This key contains {{ count_revoked }} revoked identities, which are not published. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)");
t!(
"This key contains one identity that could not be parsed as an email address.<br /> This identity can't be published on <span class=\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-email-uids\" target=\"_blank\">Why?</a>)"
);
t!(
"This key contains {{ count_unparsed }} identities that could not be parsed as an email address.<br /> These identities can't be published on <span class=\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-email-uids\" target=\"_blank\">Why?</a>)"
);
t!(
"This key contains one revoked identity, which is not published. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
);
t!(
"This key contains {{ count_revoked }} revoked identities, which are not published. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
);
t!("Your keys have been successfully uploaded:");
t!("<strong>Note:</strong> To make keys searchable by email address, you must upload them individually.");
t!(
"<strong>Note:</strong> To make keys searchable by email address, you must upload them individually."
);
t!("Verifying your email address…");
t!("If the process doesn't complete after a few seconds, please <input type=\"submit\" class=\"textbutton\" value=\"click here\" />.");
t!(
"If the process doesn't complete after a few seconds, please <input type=\"submit\" class=\"textbutton\" value=\"click here\" />."
);
t!("Manage your key on {{domain}}");
t!("Hi,");
t!("This is an automated message from <a href=\"{{base_uri}}\" style=\"text-decoration:none; color: #333\">{{domain}}</a>.");
t!(
"This is an automated message from <a href=\"{{base_uri}}\" style=\"text-decoration:none; color: #333\">{{domain}}</a>."
);
t!("If you didn't request this message, please ignore it.");
t!("OpenPGP key: <tt>{{primary_fp}}</tt>");
t!("To manage and delete listed addresses on this key, please follow the link below:");
@@ -74,10 +118,14 @@ fn _dummy() {
t!("Verify {{userid}} for your key on {{domain}}");
t!("Hi,");
t!("This is an automated message from <a href=\"{{base_uri}}\" style=\"text-decoration:none; color: #333\">{{domain}}</a>.");
t!(
"This is an automated message from <a href=\"{{base_uri}}\" style=\"text-decoration:none; color: #333\">{{domain}}</a>."
);
t!("If you didn't request this message, please ignore it.");
t!("OpenPGP key: <tt>{{primary_fp}}</tt>");
t!("To let others find this key from your email address \"<a rel=\"nofollow\" href=\"#\" style=\"text-decoration:none; color: #333\">{{userid}}</a>\", please click the link below:");
t!(
"To let others find this key from your email address \"<a rel=\"nofollow\" href=\"#\" style=\"text-decoration:none; color: #333\">{{userid}}</a>\", please click the link below:"
);
t!("You can find more info at <a href=\"{{base_uri}}/about\">{{domain}}/about</a>.");
t!("distributing OpenPGP keys since 2019");
@@ -85,7 +133,9 @@ fn _dummy() {
t!("This is an automated message from {{domain}}.");
t!("If you didn't request this message, please ignore it.");
t!("OpenPGP key: {{primary_fp}}");
t!("To let others find this key from your email address \"{{userid}}\",\nplease follow the link below:");
t!(
"To let others find this key from your email address \"{{userid}}\",\nplease follow the link below:"
);
t!("You can find more info at {{base_uri}}/about");
t!("distributing OpenPGP keys since 2019");
}

View File

@@ -1,10 +1,9 @@
use handlebars::{
Context, Handlebars, Helper, HelperDef, HelperResult, Output, RenderContext, RenderError
use rocket_dyn_templates::handlebars::{
Context, Handlebars, Helper, HelperDef, HelperResult, Output, RenderContext, RenderError,
};
use std::io;
pub struct I18NHelper {
catalogs: Vec<(&'static str, gettext::Catalog)>,
}
@@ -14,14 +13,12 @@ impl I18NHelper {
Self { catalogs }
}
pub fn get_catalog(
&self,
lang: &str,
) -> &gettext::Catalog {
let (_, ref catalog) = self.catalogs
pub fn get_catalog(&self, lang: &str) -> &gettext::Catalog {
let (_, catalog) = self
.catalogs
.iter()
.find(|(candidate, _)| *candidate == lang)
.unwrap_or_else(|| self.catalogs.get(0).unwrap());
.unwrap_or_else(|| self.catalogs.first().unwrap());
catalog
}
@@ -53,10 +50,10 @@ impl Output for StringOutput {
impl HelperDef for I18NHelper {
fn call<'reg: 'rc, 'rc>(
&self,
h: &Helper<'reg, 'rc>,
h: &Helper<'rc>,
reg: &'reg Handlebars,
context: &'rc Context,
rcx: &mut RenderContext<'reg>,
rcx: &mut RenderContext<'reg, '_>,
out: &mut dyn Output,
) -> HelperResult {
let id = if let Some(id) = h.param(0) {
@@ -75,10 +72,8 @@ impl HelperDef for I18NHelper {
let rerender = h
.param(1)
.and_then(|p| p
.path()
.map(|v| v == "rerender")
).unwrap_or(false);
.and_then(|p| p.relative_path().map(|v| v == "rerender"))
.unwrap_or(false);
let lang = context
.data()
@@ -87,14 +82,21 @@ impl HelperDef for I18NHelper {
.as_str()
.expect("Language must be string");
let response = self.lookup(lang, &id);
fn render_error_with<E>(e: E) -> RenderError
where
E: std::error::Error + Send + Sync + 'static,
{
RenderError::from_error("Failed to render", e)
}
let response = self.lookup(lang, id);
if rerender {
let data = rcx.evaluate(context, ".", false).unwrap();
let response = reg.render_template(&response, data)
.map_err(RenderError::with)?;
out.write(&response).map_err(RenderError::with)?;
let data = rcx.evaluate(context, "this").unwrap();
let response = reg
.render_template(response, data.as_json())
.map_err(render_error_with)?;
out.write(&response).map_err(render_error_with)?;
} else {
out.write(&response).map_err(RenderError::with)?;
out.write(response).map_err(render_error_with)?;
}
Ok(())
}

View File

@@ -1,16 +1,21 @@
use rocket_i18n::I18n;
use crate::database::Query;
use gettext_macros::i18n;
use hagrid_database::Query;
use rocket_i18n::I18n;
pub fn describe_query_error(i18n: &I18n, q: &Query) -> String {
match q {
Query::ByFingerprint(fpr) =>
i18n!(i18n.catalog, "No key found for fingerprint {fingerprint}"; fingerprint = fpr),
Query::ByKeyID(key_id) =>
i18n!(i18n.catalog, "No key found for key id {key_id}"; key_id = key_id),
Query::ByEmail(email) =>
i18n!(i18n.catalog, "No key found for email address {email}"; email = email),
Query::InvalidShort() => i18n!(i18n.catalog, "Search by Short Key ID is not supported."),
Query::ByFingerprint(fpr) => {
i18n!(i18n.catalog, "No key found for fingerprint {}"; fpr)
}
Query::ByKeyID(key_id) => {
i18n!(i18n.catalog, "No key found for key id {}"; key_id)
}
Query::ByEmail(email) => {
i18n!(i18n.catalog, "No key found for email address {}"; email)
}
Query::InvalidShort() => {
i18n!(i18n.catalog, "Search by Short Key ID is not supported.")
}
Query::Invalid() => i18n!(i18n.catalog, "Invalid search query."),
}
}

View File

@@ -0,0 +1,18 @@
use crate::app::config::Configuration;
use hagrid_database::Sqlite;
use rocket::Build;
use rocket::Rocket;
pub fn init(config: &Configuration) -> super::Result<Sqlite> {
let keys_internal_dir = config.keys_internal_dir.as_path();
Sqlite::new_file(
Sqlite::db_file_path(keys_internal_dir),
Sqlite::log_dir_path(keys_internal_dir),
)
.map(Some)
}
pub fn register(rocket: Rocket<Build>, _config: &Configuration, state: Sqlite) -> Rocket<Build> {
rocket.manage(state)
}

17
src/initializers/i18n.rs Normal file
View File

@@ -0,0 +1,17 @@
use crate::app::config::Configuration;
use rocket::Build;
use rocket::Rocket;
pub fn init(
_config: &Configuration,
) -> super::InfallibleResult<Vec<(&'static str, gettext::Catalog)>> {
Ok(Some(crate::get_i18n()))
}
pub fn register(
rocket: Rocket<Build>,
_config: &Configuration,
state: Vec<(&'static str, gettext::Catalog)>,
) -> Rocket<Build> {
rocket.manage(state)
}

View File

@@ -0,0 +1,23 @@
use crate::app::config::Configuration;
use crate::template_helpers::TemplateOverrides;
use rocket::Build;
use rocket::Rocket;
pub fn init(config: &Configuration) -> super::Result<TemplateOverrides> {
let localized_template_list = TemplateOverrides::load(
config.template_dir.as_path(),
crate::app::config::DEFAULT_LOCALIZED_TEMPLATE_DIR_NAME,
);
println!("{:?}", localized_template_list);
localized_template_list.map(Some)
}
pub fn register(
rocket: Rocket<Build>,
_config: &Configuration,
state: TemplateOverrides,
) -> Rocket<Build> {
rocket.manage(state)
}

View File

@@ -0,0 +1,30 @@
use crate::app::config::Configuration;
use crate::mail;
use rocket::Build;
use rocket::Rocket;
pub fn init(config: &Configuration) -> super::Result<mail::Service> {
let Configuration {
from,
base_uri,
email_template_dir: tmpl_dir,
..
} = config;
(if let Some(path) = config.filemail_into.as_deref() {
mail::Service::filemail(from, base_uri, tmpl_dir, path)
} else if let Some(true) = config.local_smtp {
mail::Service::localsmtp(from, base_uri, tmpl_dir)
} else {
mail::Service::sendmail(from, base_uri, tmpl_dir)
})
.map(Some)
}
pub fn register(
rocket: Rocket<Build>,
_config: &Configuration,
state: mail::Service,
) -> Rocket<Build> {
rocket.manage(state)
}

View File

@@ -0,0 +1,16 @@
use crate::app::config::Configuration;
use crate::web::maintenance::MaintenanceMode;
use rocket::Build;
use rocket::Rocket;
pub fn init(config: &Configuration) -> super::InfallibleResult<MaintenanceMode> {
Ok(Some(MaintenanceMode::new(config.maintenance_file.clone())))
}
pub fn register(
rocket: Rocket<Build>,
_config: &Configuration,
fairing: MaintenanceMode,
) -> Rocket<Build> {
rocket.attach(fairing)
}

59
src/initializers/mod.rs Normal file
View File

@@ -0,0 +1,59 @@
mod db_service;
mod i18n;
mod localized_template_list;
mod mail_service;
mod maintenance_mode;
mod prometheus;
mod rate_limiter;
mod routes;
pub mod state;
mod stateful_token_service;
mod stateless_token_service;
mod template;
use crate::app::config::Configuration;
use rocket::Build;
use rocket::Rocket;
// GOTCHA:
// Initializer's `init` fn can fail and return error or initializer can be disabled e.g.,
// by configuration in that case it has nothing to register in rocket i.e.,
// `init` returns Ok(None)
type Result<T, E = anyhow::Error> = anyhow::Result<Option<T>, E>;
type InfallibleResult<T> = Result<T, std::convert::Infallible>;
macro_rules! initialize {
( rocket => $rocket:ident, config => $config:ident; $( $module:ident ),+ $(,)? ) => {{
let rocket = $rocket;
let config = &$config;
$(
let rocket = if let Some(value) = $module::init(config)? {
$module::register(rocket, config, value)
} else {
rocket
};
)+
rocket
}};
}
pub fn run(rocket: Rocket<Build>, config: &Configuration) -> anyhow::Result<Rocket<Build>> {
Ok(initialize!(rocket => rocket, config => config;
// If you add a new initializer module under `hagrid::initializers`
// (e.g. `src/initializers/your_new_initializer_module.rs`), add its initializer
// name to this list so it is actually executed.
template,
maintenance_mode,
i18n,
state,
stateless_token_service,
stateful_token_service,
mail_service,
db_service,
rate_limiter,
localized_template_list,
routes,
prometheus,
))
}

View File

@@ -0,0 +1,27 @@
use crate::app::config::Configuration;
use crate::counters;
use rocket::Build;
use rocket::Rocket;
use rocket_prometheus::PrometheusMetrics;
pub fn init(config: &Configuration) -> super::InfallibleResult<PrometheusMetrics> {
if !config.enable_prometheus {
return Ok(None);
}
let prometheus = PrometheusMetrics::new();
counters::register_counters(prometheus.registry());
Ok(Some(prometheus))
}
pub fn register(
rocket: Rocket<Build>,
_config: &Configuration,
prometheus_fairing_and_metrics_routes: PrometheusMetrics,
) -> Rocket<Build> {
rocket
.attach(prometheus_fairing_and_metrics_routes.clone())
.mount("/metrics", prometheus_fairing_and_metrics_routes)
}

View File

@@ -0,0 +1,16 @@
use crate::app::config::Configuration;
use crate::rate_limiter::RateLimiter;
use rocket::Build;
use rocket::Rocket;
pub fn init(config: &Configuration) -> super::InfallibleResult<RateLimiter> {
Ok(Some(RateLimiter::new(config.mail_rate_limit)))
}
pub fn register(
rocket: Rocket<Build>,
_config: &Configuration,
state: RateLimiter,
) -> Rocket<Build> {
rocket.manage(state)
}

View File

@@ -0,0 +1,16 @@
use crate::app::config::Configuration;
use crate::routes;
use rocket::Rocket;
use rocket::{Build, Route};
pub fn init(_config: &Configuration) -> super::InfallibleResult<Vec<Route>> {
Ok(Some(routes::routes()))
}
pub fn register(
rocket: Rocket<Build>,
_config: &Configuration,
routes: Vec<Route>,
) -> Rocket<Build> {
rocket.mount("/", routes)
}

20
src/initializers/state.rs Normal file
View File

@@ -0,0 +1,20 @@
use crate::app::config::Configuration;
use crate::app::state::ApplicationState;
use rocket::Build;
use rocket::Rocket;
pub fn init(config: &Configuration) -> super::InfallibleResult<ApplicationState> {
Ok(Some(ApplicationState {
assets_dir: config.assets_dir.clone(),
base_uri: config.base_uri.clone(),
base_uri_onion: config.base_uri_onion().to_owned(),
}))
}
pub fn register(
rocket: Rocket<Build>,
_config: &Configuration,
state: ApplicationState,
) -> Rocket<Build> {
rocket.manage(state)
}

View File

@@ -0,0 +1,16 @@
use crate::app::config::Configuration;
use hagrid_database::StatefulTokens;
use rocket::Build;
use rocket::Rocket;
pub fn init(config: &Configuration) -> super::Result<StatefulTokens> {
StatefulTokens::new(config.token_dir.as_path()).map(Some)
}
pub fn register(
rocket: Rocket<Build>,
_config: &Configuration,
state: StatefulTokens,
) -> Rocket<Build> {
rocket.manage(state)
}

View File

@@ -0,0 +1,15 @@
use crate::app::config::Configuration;
use crate::tokens::Service;
use rocket::Build;
use rocket::Rocket;
pub fn init(config: &Configuration) -> super::InfallibleResult<Service> {
Ok(Some(Service::init(
&config.token_secret,
config.token_validity,
)))
}
pub fn register(rocket: Rocket<Build>, _config: &Configuration, state: Service) -> Rocket<Build> {
rocket.manage(state)
}

View File

@@ -0,0 +1,24 @@
use crate::app::config::Configuration;
use crate::i18n::I18NHelper;
use rocket::Build;
use rocket::Rocket;
use rocket::fairing::Fairing;
use rocket_dyn_templates::{Engines, Template};
pub fn init(_config: &Configuration) -> super::InfallibleResult<impl Fairing> {
Ok(Some(Template::custom(|engines: &mut Engines| {
let i18ns = crate::get_i18n();
let i18n_helper = I18NHelper::new(i18ns);
engines
.handlebars
.register_helper("text", Box::new(i18n_helper));
})))
}
pub fn register(
rocket: Rocket<Build>,
_config: &Configuration,
fairing: impl Fairing,
) -> Rocket<Build> {
rocket.attach(fairing)
}

33
src/lib.rs Normal file
View File

@@ -0,0 +1,33 @@
#![recursion_limit = "1024"]
#[cfg(debug_assertions)]
gettext_macros::init_i18n!("hagrid", en, de, ja);
#[cfg(not(debug_assertions))]
gettext_macros::init_i18n!(
"hagrid", en, de, fr, it, ja, nb, pl, tr, zh_Hans, ko, nl, ru, ar, sv, es, ro
);
mod anonymize_utils;
pub mod app;
mod counters;
mod dump;
mod gettext_strings;
mod i18n;
mod i18n_helpers;
mod initializers;
mod mail;
mod rate_limiter;
mod routes;
mod sealed_state;
mod template_helpers;
mod tokens;
mod web;
gettext_macros::compile_i18n!();
// The include_i18n macro must be called after compile_i18n, which must be called after i18n macros
// *in compilation order*. We use a helper function here to make this order consistent.
pub fn get_i18n() -> Vec<(&'static str, gettext::Catalog)> {
gettext_macros::include_i18n!()
}

View File

@@ -1,25 +1,22 @@
use std::path::{PathBuf, Path};
use anyhow;
use handlebars::Handlebars;
use lettre::{Transport as LettreTransport, SendmailTransport, file::FileTransport};
use lettre::builder::{EmailBuilder, PartBuilder, Mailbox, MimeMultipartType};
use url;
use serde::Serialize;
use uuid::Uuid;
use crate::counters;
use anyhow::anyhow;
use lettre::message::{Mailbox, MultiPart, SinglePart, header};
use lettre::{FileTransport, SendmailTransport, SmtpTransport, Transport as LettreTransport};
use rocket_dyn_templates::handlebars::Handlebars;
use serde::Serialize;
use std::path::{Path, PathBuf};
use uuid::Uuid;
use rocket_i18n::I18n;
use gettext_macros::i18n;
use rfc2047::rfc2047_encode;
use rocket_i18n::I18n;
use crate::template_helpers;
use crate::database::types::Email;
use crate::Result;
use hagrid_database::types::Email;
mod context {
use serde_derive::Serialize;
#[derive(Serialize, Clone)]
pub struct Verification {
pub lang: String,
@@ -52,34 +49,62 @@ mod context {
pub struct Service {
from: Mailbox,
domain: String,
templates: Handlebars,
templates: Handlebars<'static>,
transport: Transport,
}
enum Transport {
LocalSmtp,
Sendmail,
Filemail(PathBuf),
}
impl Service {
/// Sends mail via sendmail.
pub fn sendmail(from: &str, base_uri: &str, template_dir: &Path) -> Result<Self> {
pub fn sendmail(from: &str, base_uri: &str, template_dir: &Path) -> anyhow::Result<Self> {
Self::new(from, base_uri, template_dir, Transport::Sendmail)
}
/// Sends mail by storing it in the given directory.
pub fn filemail(from: &str, base_uri: &str, template_dir: &Path, path: &Path) -> Result<Self> {
Self::new(from, base_uri, template_dir, Transport::Filemail(path.to_owned()))
/// Sends mail via local smtp server.
pub fn localsmtp(from: &str, base_uri: &str, template_dir: &Path) -> anyhow::Result<Self> {
Self::new(from, base_uri, template_dir, Transport::LocalSmtp)
}
fn new(from: &str, base_uri: &str, template_dir: &Path, transport: Transport)
-> Result<Self> {
/// Sends mail by storing it in the given directory.
pub fn filemail(
from: &str,
base_uri: &str,
template_dir: &Path,
path: &Path,
) -> anyhow::Result<Self> {
Self::new(
from,
base_uri,
template_dir,
Transport::Filemail(path.to_owned()),
)
}
fn new(
from: &str,
base_uri: &str,
template_dir: &Path,
transport: Transport,
) -> anyhow::Result<Self> {
let templates = template_helpers::load_handlebars(template_dir)?;
let domain =
url::Url::parse(base_uri)
?.host_str().ok_or_else(|| anyhow!("No host in base-URI"))
?.to_string();
Ok(Self { from: from.into(), domain, templates, transport })
let domain = url::Url::parse(base_uri)?
.host_str()
.ok_or_else(|| anyhow!("No host in base-URI"))?
.to_string();
let from = from
.parse()
.map_err(|_| anyhow!("From must be valid email address"))?;
Ok(Self {
from,
domain,
templates,
transport,
})
}
pub fn send_verification(
@@ -88,8 +113,8 @@ impl Service {
base_uri: &str,
tpk_name: String,
userid: &Email,
token: &str
) -> Result<()> {
token: &str,
) -> anyhow::Result<()> {
let ctx = context::Verification {
lang: i18n.lang.to_string(),
primary_fp: tpk_name,
@@ -102,13 +127,13 @@ impl Service {
counters::inc_mail_sent("verify", userid);
self.send(
&vec![userid],
&[userid],
&i18n!(
i18n.catalog,
context = "Subject for verification email",
"Verify {userid} for your key on {domain}";
userid = userid,
domain = self.domain
context = "Subject for verification email, {0} = userid, {1} = keyserver domain",
"Verify {0} for your key on {1}";
userid,
self.domain.as_str(),
),
"verify",
i18n.lang,
@@ -123,7 +148,7 @@ impl Service {
tpk_name: String,
recipient: &Email,
link_path: &str,
) -> Result<()> {
) -> anyhow::Result<()> {
let ctx = context::Manage {
lang: i18n.lang.to_string(),
primary_fp: tpk_name,
@@ -138,9 +163,9 @@ impl Service {
&[recipient],
&i18n!(
i18n.catalog,
context = "Subject for manage email",
"Manage your key on {domain}";
domain = self.domain
context = "Subject for manage email, {} = keyserver domain",
"Manage your key on {}";
self.domain.as_str()
),
"manage",
i18n.lang,
@@ -153,8 +178,8 @@ impl Service {
base_uri: &str,
tpk_name: String,
userid: &Email,
token: &str
) -> Result<()> {
token: &str,
) -> anyhow::Result<()> {
let ctx = context::Welcome {
lang: "en".to_owned(),
primary_fp: tpk_name,
@@ -166,7 +191,7 @@ impl Service {
counters::inc_mail_sent("welcome", userid);
self.send(
&vec![userid],
&[userid],
&format!("Your key upload on {domain}", domain = self.domain),
"welcome",
"en",
@@ -178,12 +203,16 @@ impl Service {
&self,
template: &str,
locale: &str,
ctx: impl Serialize
) -> Result<(String, String)> {
let html = self.templates.render(&format!("{}/{}.htm", locale, template), &ctx)
ctx: impl Serialize,
) -> anyhow::Result<(String, String)> {
let html = self
.templates
.render(&format!("{}/{}.htm", locale, template), &ctx)
.or_else(|_| self.templates.render(&format!("{}.htm", template), &ctx))
.map_err(|_| anyhow!("Email template failed to render"))?;
let txt = self.templates.render(&format!("{}/{}.txt", locale, template), &ctx)
let txt = self
.templates
.render(&format!("{}/{}.txt", locale, template), &ctx)
.or_else(|_| self.templates.render(&format!("{}.txt", template), &ctx))
.map_err(|_| anyhow!("Email template failed to render"))?;
@@ -192,88 +221,77 @@ impl Service {
fn send(
&self,
to: &[&Email],
tos: &[&Email],
subject: &str,
template: &str,
locale: &str,
ctx: impl Serialize
) -> Result<()> {
ctx: impl Serialize,
) -> anyhow::Result<()> {
let (html, txt) = self.render_template(template, locale, ctx)?;
if cfg!(debug_assertions) {
for recipient in to.iter() {
println!("To: {}", recipient.to_string());
for recipient in tos.iter() {
println!("To: {}", recipient);
}
println!("{}", &txt);
}
// build this ourselves, as a temporary workaround for https://github.com/lettre/lettre/issues/400
let text = PartBuilder::new()
.body(txt)
.header(("Content-Type", "text/plain; charset=utf-8"))
.header(("Content-Transfer-Encoding", "8bit"))
.build();
let html = PartBuilder::new()
.body(html)
.header(("Content-Type", "text/html; charset=utf-8"))
.header(("Content-Transfer-Encoding", "8bit"))
.build();
let email = EmailBuilder::new()
let mut email = lettre::Message::builder()
.from(self.from.clone())
.subject(rfc2047_encode(subject))
.message_id(format!("<{}@{}>", Uuid::new_v4(), self.domain))
.message_type(MimeMultipartType::Alternative)
.header(("Content-Transfer-Encoding", "8bit"))
.child(text)
.child(html);
.subject(subject)
.message_id(Some(format!("<{}@{}>", Uuid::new_v4(), self.domain)))
.header(header::ContentTransferEncoding::EightBit);
let email = to.iter().fold(email, |email, to| email.to(to.to_string()));
for to in tos.iter() {
email = email.to(to.as_str().parse().unwrap());
}
let email = email.build()?;
let email = email.multipart(
MultiPart::alternative()
.singlepart(
SinglePart::builder()
.header(header::ContentTransferEncoding::EightBit)
.header(header::ContentType::TEXT_PLAIN)
.body(txt),
)
.singlepart(
SinglePart::builder()
.header(header::ContentTransferEncoding::EightBit)
.header(header::ContentType::TEXT_HTML)
.body(html),
),
)?;
match self.transport {
Transport::LocalSmtp => {
let transport = SmtpTransport::unencrypted_localhost();
transport.send(&email)?;
}
Transport::Sendmail => {
let mut transport = SendmailTransport::new();
transport.send(email)?;
},
let transport = SendmailTransport::new();
transport.send(&email)?;
}
Transport::Filemail(ref path) => {
let mut transport = FileTransport::new(path);
transport.send(email)?;
},
let transport = FileTransport::new(path);
transport.send(&email)?;
}
}
Ok(())
}
}
// for some reason, this is no longer public in lettre itself
// FIXME replace with builtin struct on lettre update
// see https://github.com/lettre/lettre/blob/master/lettre/src/file/mod.rs#L41
#[cfg(test)]
#[derive(Deserialize)]
struct SerializableEmail {
#[serde(alias = "envelope")]
_envelope: lettre::Envelope,
#[serde(alias = "message_id")]
_message_id: String,
message: Vec<u8>,
}
/// Returns and removes the first mail it finds from the given
/// directory.
#[cfg(test)]
pub fn pop_mail(dir: &Path) -> Result<Option<String>> {
use std::fs;
pub fn pop_mail(dir: &Path) -> anyhow::Result<Option<String>> {
use std::{fs, fs::read_to_string};
for entry in fs::read_dir(dir)? {
let entry = entry?;
if entry.file_type()?.is_file() {
let fh = fs::File::open(entry.path())?;
let body = read_to_string(entry.path())?.replace("\r\n", "\n");
fs::remove_file(entry.path())?;
let mail: SerializableEmail = ::serde_json::from_reader(fh)?;
let body = String::from_utf8_lossy(&mail.message).to_string();
println!("{}", body);
return Ok(Some(body));
}
}
@@ -282,23 +300,34 @@ pub fn pop_mail(dir: &Path) -> Result<Option<String>> {
#[cfg(test)]
mod test {
use crate::get_i18n;
use super::*;
use tempfile::{tempdir, TempDir};
use gettext_macros::{include_i18n};
use std::str::FromStr;
use tempfile::{TempDir, tempdir};
const BASEDIR: &str = "http://localhost/";
const FROM: &str = "test@localhost";
const TO: &str = "recipient@example.org";
fn configure_i18n(lang: &'static str) -> I18n {
let langs = include_i18n!();
let catalog = langs.clone().into_iter().find(|(l, _)| *l == lang).unwrap().1;
let langs = get_i18n();
let catalog = langs
.clone()
.into_iter()
.find(|(l, _)| *l == lang)
.unwrap()
.1;
rocket_i18n::I18n { catalog, lang }
}
fn configure_mail() -> (Service, TempDir) {
let template_dir: PathBuf = ::std::env::current_dir().unwrap().join("dist/email-templates").to_str().unwrap().into();
let template_dir: PathBuf = ::std::env::current_dir()
.unwrap()
.join("dist/email-templates")
.to_str()
.unwrap()
.into();
let tempdir = tempdir().unwrap();
let service = Service::filemail(FROM, BASEDIR, &template_dir, tempdir.path()).unwrap();
(service, tempdir)
@@ -308,7 +337,7 @@ mod test {
if let Some((_, v)) = headers.iter().find(|(h, _)| *h == name) {
assert!(pred(v));
} else {
panic!(format!("Missing header: {}", name));
panic!("Missing header: {}", name);
}
}
@@ -318,20 +347,16 @@ mod test {
let headers: Vec<_> = mail_content
.lines()
.filter(|line| line.contains(": "))
.map(|line| {
let mut it = line.splitn(2, ": ");
let h = it.next().unwrap();
let v = it.next().unwrap();
(h, v)
})
.map(|line| line.split_once(": ").unwrap())
.collect();
assert!(headers.contains(&("Content-Transfer-Encoding", "8bit")));
assert!(headers.contains(&("Content-Type", "text/plain; charset=utf-8")));
assert!(headers.contains(&("Content-Type", "text/html; charset=utf-8")));
assert!(headers.contains(&("From", "<test@localhost>")));
assert!(headers.contains(&("To", "<recipient@example.org>")));
assert_header(&headers, "Content-Type", |v| v.starts_with("multipart/alternative"));
assert_header(&headers, "Date", |v| v.contains("+0000"));
assert!(headers.contains(&("From", "test@localhost")));
assert!(headers.contains(&("To", "recipient@example.org")));
assert_header(&headers, "Content-Type", |v| {
v.starts_with("multipart/alternative")
});
assert_header(&headers, "Date", |v| v.contains("-0000"));
assert_header(&headers, "Message-ID", |v| v.contains("@localhost>"));
}
@@ -347,7 +372,14 @@ mod test {
let i18n = configure_i18n("en");
let recipient = Email::from_str(TO).unwrap();
mail.send_verification(&i18n, "test", "fingerprintoo".to_owned(), &recipient, "token").unwrap();
mail.send_verification(
&i18n,
"test",
"fingerprintoo".to_owned(),
&recipient,
"token",
)
.unwrap();
let mail_content = pop_mail(tempdir.path()).unwrap().unwrap();
check_headers(&mail_content);
@@ -365,7 +397,14 @@ mod test {
let i18n = configure_i18n("ja");
let recipient = Email::from_str(TO).unwrap();
mail.send_verification(&i18n, "test", "fingerprintoo".to_owned(), &recipient, "token").unwrap();
mail.send_verification(
&i18n,
"test",
"fingerprintoo".to_owned(),
&recipient,
"token",
)
.unwrap();
let mail_content = pop_mail(tempdir.path()).unwrap().unwrap();
check_headers(&mail_content);
@@ -375,8 +414,9 @@ mod test {
assert!(mail_content.contains("test/verify/token"));
assert!(mail_content.contains("test/about"));
assert!(mail_content.contains("あなたのメールアド"));
assert!(mail_content.contains("Subject: =?utf-8?q?localhost=E3=81=AE=E3=81=82=E3=81=AA=E3=81=9F=E3=81=AE?="));
assert!(mail_content.contains(
"Subject: =?utf-8?b?bG9jYWxob3N044Gu44GC44Gq44Gf44Gu6Y2144Gu44Gf44KB44GrbG9jYWxob3N044KS5qSc6Ki844GZ44KL?="
));
}
#[test]
@@ -385,7 +425,14 @@ mod test {
let i18n = configure_i18n("en");
let recipient = Email::from_str(TO).unwrap();
mail.send_manage_token(&i18n, "test", "fingerprintoo".to_owned(), &recipient, "token").unwrap();
mail.send_manage_token(
&i18n,
"test",
"fingerprintoo".to_owned(),
&recipient,
"token",
)
.unwrap();
let mail_content = pop_mail(tempdir.path()).unwrap().unwrap();
check_headers(&mail_content);
@@ -403,7 +450,14 @@ mod test {
let i18n = configure_i18n("ja");
let recipient = Email::from_str(TO).unwrap();
mail.send_manage_token(&i18n, "test", "fingerprintoo".to_owned(), &recipient, "token").unwrap();
mail.send_manage_token(
&i18n,
"test",
"fingerprintoo".to_owned(),
&recipient,
"token",
)
.unwrap();
let mail_content = pop_mail(tempdir.path()).unwrap().unwrap();
check_headers(&mail_content);
@@ -414,7 +468,9 @@ mod test {
assert!(mail_content.contains("testtoken"));
assert!(mail_content.contains("test/about"));
assert!(mail_content.contains("この鍵の掲示されたア"));
assert!(mail_content.contains("Subject: =?utf-8?q?localhost=E3=81=AE=E9=8D=B5=E3=82=92=E7=AE=A1=E7=90=86?="));
assert!(
mail_content.contains("Subject: =?utf-8?b?bG9jYWxob3N044Gu6Y2144KS566h55CG44GZ44KL?=")
);
}
#[test]
@@ -422,7 +478,8 @@ mod test {
let (mail, tempdir) = configure_mail();
let recipient = Email::from_str(TO).unwrap();
mail.send_welcome("test", "fingerprintoo".to_owned(), &recipient, "token").unwrap();
mail.send_welcome("test", "fingerprintoo".to_owned(), &recipient, "token")
.unwrap();
let mail_content = pop_mail(tempdir.path()).unwrap().unwrap();
check_headers(&mail_content);
@@ -434,4 +491,3 @@ mod test {
assert!(mail_content.contains("first time"));
}
}

View File

@@ -1,54 +1,3 @@
#![feature(proc_macro_hygiene, plugin, decl_macro)]
#![recursion_limit = "1024"]
#[macro_use]
extern crate anyhow;
use anyhow::Result as Result;
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate rocket;
#[macro_use]
extern crate rocket_contrib;
#[cfg(test)]
extern crate regex;
extern crate hagrid_database as database;
use gettext_macros::init_i18n;
#[cfg(debug_assertions)]
init_i18n!("hagrid", en, de, ja);
#[cfg(not(debug_assertions))]
init_i18n!("hagrid", en, de, fr, it, ja, nb, pl, tr, zh_Hans, ko, nl, ru, ar);
mod mail;
mod anonymize_utils;
mod tokens;
mod sealed_state;
mod rate_limiter;
mod dump;
mod counters;
mod i18n;
mod i18n_helpers;
mod gettext_strings;
mod web;
mod template_helpers;
fn main() {
if let Err(e) = web::serve() {
eprint!("{}", e);
let mut cause = e.source();
while let Some(c) = cause {
eprint!(":\n {}", c);
cause = c.source();
}
eprintln!();
::std::process::exit(2);
}
hagrid::app::run()
}

View File

@@ -1,6 +1,6 @@
use std::sync::Mutex;
use std::collections::HashMap;
use std::time::{Instant,Duration};
use std::sync::Mutex;
use std::time::{Duration, Instant};
pub struct RateLimiter {
locked_map: Mutex<HashMap<String, Instant>>,
@@ -23,11 +23,12 @@ impl RateLimiter {
self.maybe_cleanup();
let mut locked_map = self.locked_map.lock().unwrap();
let action_ok = locked_map.get(&identifier)
let action_ok = locked_map
.get(&identifier)
.map(|instant| instant.elapsed())
.map(|duration| duration >= self.timeout)
.unwrap_or(true);
if action_ok {
if action_ok {
locked_map.insert(identifier, Instant::now());
}
action_ok
@@ -35,7 +36,8 @@ impl RateLimiter {
pub fn action_check(&self, identifier: String) -> bool {
let locked_map = self.locked_map.lock().unwrap();
locked_map.get(&identifier)
locked_map
.get(&identifier)
.map(|instant| instant.elapsed())
.map(|duration| duration >= self.timeout)
.unwrap_or(true)
@@ -65,7 +67,7 @@ mod tests {
assert!(rate_limiter.action_perform("action".to_owned()));
assert_eq!(false, rate_limiter.action_perform("action".to_owned()));
assert!(!rate_limiter.action_perform("action".to_owned()));
}
#[test]

111
src/routes/about.rs Normal file
View File

@@ -0,0 +1,111 @@
use crate::web::{MyResponse, RequestOrigin};
use rocket_codegen::get;
use rocket_i18n::I18n;
#[get("/about")]
pub fn about(origin: RequestOrigin, i18n: I18n) -> MyResponse {
MyResponse::ok_bare("about/about", i18n, origin)
}
#[get("/about/news")]
pub fn news(origin: RequestOrigin, i18n: I18n) -> MyResponse {
MyResponse::ok_bare("about/news", i18n, origin)
}
#[get("/about/faq")]
pub fn faq(origin: RequestOrigin, i18n: I18n) -> MyResponse {
MyResponse::ok_bare("about/faq", i18n, origin)
}
#[get("/about/usage")]
pub fn usage(origin: RequestOrigin, i18n: I18n) -> MyResponse {
MyResponse::ok_bare("about/usage", i18n, origin)
}
#[get("/about/privacy")]
pub fn privacy(origin: RequestOrigin, i18n: I18n) -> MyResponse {
MyResponse::ok_bare("about/privacy", i18n, origin)
}
#[get("/about/api")]
pub fn apidoc(origin: RequestOrigin, i18n: I18n) -> MyResponse {
MyResponse::ok_bare("about/api", i18n, origin)
}
#[get("/about/stats")]
pub fn stats(origin: RequestOrigin, i18n: I18n) -> MyResponse {
MyResponse::ok_bare("about/stats", i18n, origin)
}
#[cfg(test)]
mod tests {
use crate::routes::tests::common::*;
use ::rocket::http::{ContentType, Header, Status};
use ::rocket::local::blocking::Client;
use rstest::rstest;
use tempfile::TempDir;
mod get_about {
use super::*;
const URI: &str = "/about";
#[rstest]
fn landing_page_is_visible_with_translations(
#[from(client)] (_tmpdir, client): (TempDir, Client),
) {
assert::response(
client
.get(URI)
.header(Header::new("Accept-Language", "de"))
.dispatch(),
Status::Ok,
ContentType::HTML,
"Hagrid",
);
// TODO check translation
}
#[rstest]
fn privacy_policy_is_visible(#[from(client)] (_tmpdir, client): (TempDir, Client)) {
assert::response(
client.get(URI).dispatch(),
Status::Ok,
ContentType::HTML,
"distribution and discovery",
);
}
}
mod get_about_privacy {
use super::*;
const URI: &str = "/about/privacy";
#[rstest]
fn privacy_policy_is_visible(#[from(client)] (_tmpdir, client): (TempDir, Client)) {
assert::response(
client.get(URI).dispatch(),
Status::Ok,
ContentType::HTML,
"Public Key Data",
);
}
}
mod get_about_api {
use super::*;
const URI: &str = "/about/api";
#[rstest]
fn api_docs_are_visible(#[from(client)] (_tmpdir, client): (TempDir, Client)) {
assert::response(
client.get(URI).dispatch(),
Status::Ok,
ContentType::HTML,
"/vks/v1/by-keyid",
);
}
}
}

1
src/routes/api.rs Normal file
View File

@@ -0,0 +1 @@
pub mod rest;

View File

@@ -0,0 +1 @@
pub mod vks;

173
src/routes/api/rest/vks.rs Normal file
View File

@@ -0,0 +1,173 @@
use crate::rate_limiter::RateLimiter;
use crate::web::vks_api::{JsonErrorResponse, JsonResult, json};
use crate::web::{MyResponse, RequestOrigin, vks, vks_api};
use crate::{mail, tokens, web};
use hagrid_database::types::{Email, Fingerprint, KeyID};
use hagrid_database::{Query, Sqlite, StatefulTokens};
use rocket::http::Status;
use rocket::serde::json::{Error as JsonError, Json};
use rocket_codegen::{get, post};
use rocket_i18n::{I18n, Translations};
#[post("/vks/v1/upload", format = "json", data = "<data>")]
pub fn upload_json(
db: &rocket::State<Sqlite>,
tokens_stateless: &rocket::State<tokens::Service>,
rate_limiter: &rocket::State<RateLimiter>,
i18n: I18n,
data: Result<Json<json::UploadRequest>, JsonError>,
) -> JsonResult {
let data = vks_api::json_or_error(data)?;
use crate::web::{vks, vks_api};
use std::io::Cursor;
let data_reader = Cursor::new(data.keytext.as_bytes());
let result = vks::process_key(db, &i18n, tokens_stateless, rate_limiter, data_reader);
vks_api::upload_ok_json(result)
}
#[post("/vks/v1/upload", rank = 2)]
pub fn upload_fallback(origin: RequestOrigin) -> JsonErrorResponse {
let error_msg = format!(
"expected application/json data. see {}/about/api for api docs.",
origin.get_base_uri()
);
JsonErrorResponse(Status::BadRequest, error_msg)
}
#[post("/vks/v1/request-verify", format = "json", data = "<data>")]
pub fn request_verify_json(
db: &rocket::State<Sqlite>,
langs: &rocket::State<Translations>,
origin: RequestOrigin,
token_stateful: &rocket::State<StatefulTokens>,
token_stateless: &rocket::State<tokens::Service>,
mail_service: &rocket::State<mail::Service>,
rate_limiter: &rocket::State<RateLimiter>,
data: Result<Json<json::VerifyRequest>, JsonError>,
) -> JsonResult {
let data = vks_api::json_or_error(data)?;
let json::VerifyRequest {
token,
addresses,
locale,
} = data.into_inner();
let i18n = vks_api::get_locale(langs, locale.unwrap_or_default());
let result = vks::request_verify(
db,
&origin,
token_stateful,
token_stateless,
mail_service,
rate_limiter,
&i18n,
token,
addresses,
);
vks_api::upload_ok_json(result)
}
#[post("/vks/v1/request-verify", rank = 2)]
pub fn request_verify_fallback(origin: RequestOrigin) -> JsonErrorResponse {
let error_msg = format!(
"expected application/json data. see {}/about/api for api docs.",
origin.get_base_uri()
);
JsonErrorResponse(Status::BadRequest, error_msg)
}
#[get("/vks/v1/by-fingerprint/<fpr>")]
pub fn vks_v1_by_fingerprint(db: &rocket::State<Sqlite>, i18n: I18n, fpr: String) -> MyResponse {
let query = match fpr.parse::<Fingerprint>() {
Ok(fpr) => Query::ByFingerprint(fpr),
Err(_) => return MyResponse::bad_request_plain("malformed fingerprint"),
};
web::key_to_response_plain(db, i18n, query)
}
#[get("/vks/v1/by-email/<email>")]
pub fn vks_v1_by_email(db: &rocket::State<Sqlite>, i18n: I18n, email: String) -> MyResponse {
let email = email.replace("%40", "@");
let query = match email.parse::<Email>() {
Ok(email) => Query::ByEmail(email),
Err(_) => return MyResponse::bad_request_plain("malformed e-mail address"),
};
web::key_to_response_plain(db, i18n, query)
}
#[get("/vks/v1/by-keyid/<kid>")]
pub fn vks_v1_by_keyid(db: &rocket::State<Sqlite>, i18n: I18n, kid: String) -> MyResponse {
let query = match kid.parse::<KeyID>() {
Ok(keyid) => Query::ByKeyID(keyid),
Err(_) => return MyResponse::bad_request_plain("malformed key id"),
};
web::key_to_response_plain(db, i18n, query)
}
#[cfg(test)]
mod tests {
use crate::routes::tests::common::*;
use rocket::http::ContentType;
use rocket::local::blocking::Client;
use rstest::rstest;
use tempfile::TempDir;
mod post_vks_v1_upload {
use super::*;
const URI: &str = "/vks/v1/upload";
#[rstest]
fn maintenance(
#[from(client)] (tmpdir, client): (TempDir, Client),
maintenance_text: &str,
#[from(serialized_cert)] (_cert_name, serialized_cert): (&str, Vec<u8>),
) {
let request_closure = || {
client
.post(URI)
.header(ContentType::JSON)
.body(format!(
r#"{{ "keytext": "{}" }}"#,
base64::encode(&serialized_cert)
))
.dispatch()
};
test::maintenance(request_closure, tmpdir, ContentType::JSON, maintenance_text);
}
}
mod post_vks_v1_request_verify_json {
use super::*;
use crate::web::tests::common;
const URI: &str = "/vks/v1/request-verify";
#[rstest]
fn maintenance(
#[from(client)] (tmpdir, client): (TempDir, Client),
maintenance_text: &str,
#[from(serialized_cert)] (cert_name, serialized_cert): (&str, Vec<u8>),
) {
let token = common::assert::vks_publish_json_get_token(&client, &serialized_cert);
let json = format!(r#"{{"token":"{}","addresses":["{}"]}}"#, token, cert_name);
let request_closure = || {
client
.post(URI)
.header(ContentType::JSON)
.body(json.as_bytes())
.dispatch()
};
test::maintenance(request_closure, tmpdir, ContentType::JSON, maintenance_text);
}
}
}

9
src/routes/assets.rs Normal file
View File

@@ -0,0 +1,9 @@
use crate::app::state::ApplicationState;
use rocket::fs::NamedFile;
use rocket_codegen::get;
use std::path::PathBuf;
#[get("/assets/<file..>")]
pub async fn files(file: PathBuf, state: &rocket::State<ApplicationState>) -> Option<NamedFile> {
NamedFile::open(state.assets_dir.join(file)).await.ok()
}

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