gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[libeufin] branch master updated (bb3d2eea -> 09482f4c)


From: gnunet
Subject: [libeufin] branch master updated (bb3d2eea -> 09482f4c)
Date: Wed, 01 Mar 2023 15:40:55 +0100

This is an automated email from the git hooks/post-receive script.

ms pushed a change to branch master
in repository libeufin.

    from bb3d2eea Cascade-deleting when deleting a user.
     new 2f2277c0 Debian package.
     new 09482f4c Long polling.

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 debian/control                                     |  1 +
 .../nginx/sites-available/libeufin-sandbox.conf    | 37 ++++++++++
 debian/libeufin.install                            |  2 +
 nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt    |  1 -
 .../tech/libeufin/nexus/bankaccount/BankAccount.kt |  6 +-
 .../main/kotlin/tech/libeufin/nexus/server/JSON.kt |  2 +-
 nexus/src/test/kotlin/DBTest.kt                    | 72 +++++--------------
 nexus/src/test/kotlin/MakeEnv.kt                   | 84 +++++++++++++++++++++-
 nexus/src/test/kotlin/TalerTest.kt                 | 33 +++++++--
 util/build.gradle                                  |  2 +
 util/src/main/kotlin/DB.kt                         | 84 ++++++++++++++++++++++
 util/src/main/kotlin/DBTypes.kt                    | 76 --------------------
 util/src/main/kotlin/LibeufinErrorCodes.kt         |  4 +-
 13 files changed, 260 insertions(+), 144 deletions(-)
 create mode 100644 debian/etc/nginx/sites-available/libeufin-sandbox.conf
 create mode 100644 util/src/main/kotlin/DB.kt
 delete mode 100644 util/src/main/kotlin/DBTypes.kt

diff --git a/debian/control b/debian/control
index d15a4346..465c1b55 100644
--- a/debian/control
+++ b/debian/control
@@ -17,6 +17,7 @@ Depends: openjdk-11-jdk-headless | openjdk-11-jdk | 
openjdk-12-jdk-headless | op
          python3 (>= 3.7),
          python3-click,
          python3-requests,
+         apache2 | nginx | httpd,
          ${misc:Depends}
 Recommends:
 Description: Software package to access FinTS/EBICS based
diff --git a/debian/etc/nginx/sites-available/libeufin-sandbox.conf 
b/debian/etc/nginx/sites-available/libeufin-sandbox.conf
new file mode 100644
index 00000000..23046e90
--- /dev/null
+++ b/debian/etc/nginx/sites-available/libeufin-sandbox.conf
@@ -0,0 +1,37 @@
+server {
+  include /etc/nginx/mime.types;
+
+  # NOTE:
+  # - urgently consider configuring TLS instead
+  # - maybe keep a forwarder from HTTP to HTTPS
+  listen 80;
+
+  # NOTE:
+  # - Comment out this line if you have no IPv6
+  listen [::]:80;
+
+  # NOTE:
+  # - replace with your actual server name.
+  server_name localhost;
+
+  # Doesn't take requests away from the backend,
+  # because that expects always a "/demobanks/default"
+  # prefix.
+  rewrite ^/$ /webui/index.html;
+
+  # FRONTEND
+  location /webui {
+    # This location has both the SPA HTML and the
+    # JavaScript configuration demobank-ui-settings.js 
+    alias /usr/share/taler/demobank-ui;
+  }
+
+  # BACKEND
+  location / {
+    # NOTE: urgently change to 'https' once TLS has been configured.
+    proxy_set_header X-Forwarded-Proto "$scheme";
+    proxy_set_header X-Forwarded-Host "localhost";
+    proxy_set_header X-Forwarded-Prefix /;
+    proxy_pass http://localhost:5000/;
+  }
+}
diff --git a/debian/libeufin.install b/debian/libeufin.install
index 708dd6d4..def283a9 100644
--- a/debian/libeufin.install
+++ b/debian/libeufin.install
@@ -1 +1,3 @@
 debian/etc/* etc/
+# Only used to install the SPA + JS config:
+debian/usr/share/libeufin/* usr/share/libeufin/
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt
index 6d3f586b..b1ddbd17 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt
@@ -27,7 +27,6 @@ import 
org.jetbrains.exposed.sql.transactions.TransactionManager
 import org.jetbrains.exposed.sql.transactions.transaction
 import tech.libeufin.nexus.iso20022.EntryStatus
 import tech.libeufin.util.EbicsInitState
-import tech.libeufin.util.amount
 import java.sql.Connection
 
 /**
diff --git 
a/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt
index b32b814a..416cd4ae 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt
@@ -27,9 +27,7 @@ import org.jetbrains.exposed.sql.*
 import org.jetbrains.exposed.sql.transactions.transaction
 import org.w3c.dom.Document
 import tech.libeufin.nexus.*
-import tech.libeufin.nexus.iso20022.CamtParsingError
-import tech.libeufin.nexus.iso20022.CreditDebitIndicator
-import tech.libeufin.nexus.iso20022.parseCamtMessage
+import tech.libeufin.nexus.iso20022.*
 import tech.libeufin.nexus.server.FetchSpecJson
 import tech.libeufin.nexus.server.Pain001Data
 import tech.libeufin.nexus.server.requireBankConnection
@@ -232,7 +230,7 @@ fun processCamtMessage(
                 }
             }
         }
-        val entries = res.reports.map { it.entries }.flatten()
+        val entries: List<CamtBankAccountEntry> = res.reports.map { it.entries 
}.flatten()
         var newPaymentsLog = ""
         downloadedTransactions = entries.size
         txloop@ for (entry in entries) {
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt
index 8f87c0e2..38ab4236 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt
@@ -440,7 +440,7 @@ class CurrencyAmountSerializer(jc: Class<CurrencyAmount> = 
CurrencyAmount::class
 @JsonSerialize(using = CurrencyAmountSerializer::class)
 data class CurrencyAmount(
     val currency: String,
-    val value: String // allows calculations
+    val value: String
 )
 fun CurrencyAmount.toPlainString(): String {
     return "${this.currency}:${this.value}"
diff --git a/nexus/src/test/kotlin/DBTest.kt b/nexus/src/test/kotlin/DBTest.kt
index 3b818942..ff044a3e 100644
--- a/nexus/src/test/kotlin/DBTest.kt
+++ b/nexus/src/test/kotlin/DBTest.kt
@@ -1,67 +1,31 @@
 package tech.libeufin.nexus
 
+import kotlinx.coroutines.*
+import org.jetbrains.exposed.dao.flushCache
 import org.jetbrains.exposed.exceptions.ExposedSQLException
 import org.jetbrains.exposed.sql.*
+import org.jetbrains.exposed.sql.statements.api.ExposedConnection
+import org.jetbrains.exposed.sql.transactions.TransactionManager
 import org.jetbrains.exposed.sql.transactions.transaction
+import org.jetbrains.exposed.sql.transactions.transactionManager
 import org.junit.Test
+import org.postgresql.PGConnection
+import org.postgresql.jdbc.PgConnection
+import tech.libeufin.util.PostgresListenNotify
 import withTestDatabase
-import java.io.File
-
-object MyTable : Table() {
-    val col1 = text("col1")
-    val col2 = text("col2")
-    override val primaryKey = PrimaryKey(col1, col2)
-}
+import java.sql.Connection
+import java.sql.DriverManager
 
 class DBTest {
-    @Test(expected = ExposedSQLException::class)
-    fun sqlDslTest() {
-        withTestDatabase {
-            transaction {
-                addLogger(StdOutSqlLogger)
-                SchemaUtils.create(MyTable)
-                MyTable.insert {
-                    it[col1] = "foo"
-                    it[col2] = "bar"
-                }
-                // should throw ExposedSQLException
-                MyTable.insert {
-                    it[col1] = "foo"
-                    it[col2] = "bar"
-                }
-                MyTable.insert {  } // shouldn't it fail for non-NULL 
constraint violation?
-            }
-        }
-    }
 
+    // Testing database notifications (only postgresql)
     @Test
-    fun facadeConfigTest() {
-        withTestDatabase {
-            transaction {
-                addLogger(StdOutSqlLogger)
-                SchemaUtils.create(
-                    FacadesTable,
-                    FacadeStateTable,
-                    NexusUsersTable
-                )
-                val user = NexusUserEntity.new {
-                    username = "testuser"
-                    passwordHash = "x"
-                    superuser = true
-                }
-                val facade = FacadeEntity.new {
-                    facadeName = "testfacade"
-                    type = "any"
-                    creator = user
-                }
-                FacadeStateEntity.new {
-                    bankAccount = "b"
-                    bankConnection = "b"
-                    reserveTransferLevel = "any"
-                    this.facade = facade
-                    currency = "UNUSED"
-                }
-            }
-        }
+    fun notifications() {
+        val genCon = 
DriverManager.getConnection("jdbc:postgresql://localhost:5432/talercheck?user=job")
+        val pgCon = genCon.unwrap(org.postgresql.jdbc.PgConnection::class.java)
+        val ln = PostgresListenNotify(pgCon, "x")
+        ln.postrgesListen()
+        ln.postgresNotify()
+        runBlocking { ln.postgresWaitNotification(2000L) }
     }
 }
\ No newline at end of file
diff --git a/nexus/src/test/kotlin/MakeEnv.kt b/nexus/src/test/kotlin/MakeEnv.kt
index d2a2b480..8d391151 100644
--- a/nexus/src/test/kotlin/MakeEnv.kt
+++ b/nexus/src/test/kotlin/MakeEnv.kt
@@ -7,6 +7,8 @@ import org.jetbrains.exposed.sql.transactions.transactionManager
 import tech.libeufin.nexus.*
 import tech.libeufin.nexus.dbCreateTables
 import tech.libeufin.nexus.dbDropTables
+import tech.libeufin.nexus.iso20022.*
+import tech.libeufin.nexus.server.CurrencyAmount
 import tech.libeufin.nexus.server.FetchLevel
 import tech.libeufin.nexus.server.FetchSpecAllJson
 import tech.libeufin.sandbox.*
@@ -21,9 +23,9 @@ data class EbicsKeys(
     val sig: CryptoUtil.RsaCrtKeyPair
 )
 const val TEST_DB_FILE = "/tmp/nexus-test.sqlite3"
-const val TEST_DB_CONN = "jdbc:sqlite:$TEST_DB_FILE"
+// const val TEST_DB_CONN = "jdbc:sqlite:$TEST_DB_FILE"
 // Convenience DB connection to switch to Postgresql:
-// const val TEST_DB_CONN = "jdbc:postgresql://localhost:5432/talercheck?user="
+const val TEST_DB_CONN = "jdbc:postgresql://localhost:5432/talercheck?user=job"
 val BANK_IBAN = getIban()
 val FOO_USER_IBAN = getIban()
 val BAR_USER_IBAN = getIban()
@@ -292,4 +294,80 @@ fun withSandboxTestDatabase(f: () -> Unit) {
         }
         f()
     }
-}
\ No newline at end of file
+}
+
+fun talerIncomingForFoo(currency: String, value: String, subject: String) {
+    transaction {
+        val inc = NexusBankTransactionEntity.new {
+            bankAccount = NexusBankAccountEntity.findByName("foo")!!
+            accountTransactionId = "mock"
+            creditDebitIndicator = "CRDT"
+            this.currency = currency
+            this.amount = value
+            status = EntryStatus.BOOK
+            transactionJson = jacksonObjectMapper(
+            ).writerWithDefaultPrettyPrinter(
+            ).writeValueAsString(
+                genNexusIncomingPayment(
+                    amount = CurrencyAmount(currency,value),
+                    subject = subject
+                )
+            )
+        }
+        TalerIncomingPaymentEntity.new {
+            payment = inc
+            reservePublicKey = "mock"
+            timestampMs = 0L
+            debtorPaytoUri = "mock"
+        }
+    }
+}
+
+
+fun genNexusIncomingPayment(
+    amount: CurrencyAmount,
+    subject: String,
+): CamtBankAccountEntry =
+    CamtBankAccountEntry(
+        amount = amount,
+        creditDebitIndicator = CreditDebitIndicator.CRDT,
+        status = EntryStatus.BOOK,
+        bankTransactionCode = "mock",
+        valueDate = null,
+        bookingDate = null,
+        accountServicerRef = null,
+        entryRef = null,
+        currencyExchange = null,
+        counterValueAmount = null,
+        instructedAmount = null,
+        batches = listOf(
+            Batch(
+                paymentInformationId = null,
+                messageId = null,
+                batchTransactions = listOf(
+                    BatchTransaction(
+                        amount = amount,
+                        creditDebitIndicator = CreditDebitIndicator.CRDT,
+                        details = TransactionDetails(
+                            unstructuredRemittanceInformation = subject,
+                            debtor = null,
+                            debtorAccount = null,
+                            debtorAgent = null,
+                            creditor = null,
+                            creditorAccount = null,
+                            creditorAgent = null,
+                            ultimateCreditor = null,
+                            ultimateDebtor = null,
+                            purpose = null,
+                            proprietaryPurpose = null,
+                            currencyExchange = null,
+                            instructedAmount = null,
+                            counterValueAmount = null,
+                            interBankSettlementAmount = null,
+                            returnInfo = null
+                        )
+                    )
+                )
+            )
+        )
+    )
\ No newline at end of file
diff --git a/nexus/src/test/kotlin/TalerTest.kt 
b/nexus/src/test/kotlin/TalerTest.kt
index bc117307..36c656f1 100644
--- a/nexus/src/test/kotlin/TalerTest.kt
+++ b/nexus/src/test/kotlin/TalerTest.kt
@@ -1,25 +1,50 @@
 import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
 import io.ktor.client.plugins.*
 import io.ktor.client.request.*
+import io.ktor.client.statement.*
 import io.ktor.http.*
 import io.ktor.server.testing.*
 import kotlinx.coroutines.*
+import kotlinx.coroutines.future.future
 import org.jetbrains.exposed.sql.transactions.TransactionManager
 import org.jetbrains.exposed.sql.transactions.transaction
 import org.junit.Ignore
 import org.junit.Test
 import tech.libeufin.nexus.*
 import tech.libeufin.nexus.bankaccount.fetchBankAccountTransactions
-import tech.libeufin.nexus.server.FetchLevel
-import tech.libeufin.nexus.server.FetchSpecAllJson
-import tech.libeufin.nexus.server.client
-import tech.libeufin.nexus.server.nexusApp
+import tech.libeufin.nexus.iso20022.EntryStatus
+import tech.libeufin.nexus.server.*
 import tech.libeufin.sandbox.sandboxApp
 import tech.libeufin.sandbox.wireTransfer
 
 // This class tests the features related to the Taler facade.
 class TalerTest {
 
+    /**
+     * Tests that a client (normally represented by the wire-watch)
+     * gets incoming transactions.
+     */
+    @Test
+    fun historyIncomingTest() {
+        withNexusAndSandboxUser {
+            testApplication {
+                application(nexusApp)
+                runBlocking {
+                    val future = async {
+                        client.get(
+                            
"/facades/taler/taler-wire-gateway/history/incoming?delta=5"
+                        ) {
+                            expectSuccess = true
+                            contentType(ContentType.Application.Json)
+                            basicAuth("foo", "foo")
+                        }
+                    }
+                    talerIncomingForFoo("KUDOS", "10", "Invalid")
+                }
+            }
+        }
+    }
+
     @Ignore // Ignoring because no assert takes place.
     @Test // Triggering a refund because of a duplicate reserve pub.
     fun refundTest() {
diff --git a/util/build.gradle b/util/build.gradle
index a294ff31..4c8da032 100644
--- a/util/build.gradle
+++ b/util/build.gradle
@@ -45,6 +45,8 @@ dependencies {
     // https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-reflect
     implementation group: 'org.jetbrains.kotlin', name: 'kotlin-reflect', 
version: '1.5.21'
 
+    // Database helper
+    implementation group: 'org.postgresql', name: 'postgresql', version: 
'42.5.4'
     implementation "org.jetbrains.exposed:exposed-core:$exposed_version"
     implementation "org.jetbrains.exposed:exposed-dao:$exposed_version"
     implementation "io.netty:netty-all:$netty_version"
diff --git a/util/src/main/kotlin/DB.kt b/util/src/main/kotlin/DB.kt
new file mode 100644
index 00000000..63a213e2
--- /dev/null
+++ b/util/src/main/kotlin/DB.kt
@@ -0,0 +1,84 @@
+/*
+ * This file is part of LibEuFin.
+ * Copyright (C) 2020 Taler Systems S.A.
+ *
+ * LibEuFin is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3, or
+ * (at your option) any later version.
+ *
+ * LibEuFin is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with LibEuFin; see the file COPYING.  If not, see
+ * <http://www.gnu.org/licenses/>
+ */
+
+package tech.libeufin.util
+import UtilError
+import io.ktor.http.*
+import kotlinx.coroutines.delay
+import logger
+import org.postgresql.jdbc.PgConnection
+import java.lang.Long.max
+
+// This class abstracts the LISTEN/NOTIFY construct supported
+class PostgresListenNotify(
+    private val conn: PgConnection,
+    private val channel: String
+) {
+    fun postrgesListen() {
+        val stmt = conn.createStatement()
+        stmt.execute("LISTEN $channel")
+        stmt.close()
+    }
+    fun postgresNotify() {
+        val stmt = conn.createStatement()
+        stmt.execute("NOTIFY $channel")
+        stmt.close()
+    }
+
+    suspend fun postgresWaitNotification(timeoutMs: Long) {
+        // Splits the checks into 10ms chunks.
+        val sleepTimeMs = 10L
+        var notificationFound = false
+        val iterations = timeoutMs / sleepTimeMs
+        for (i in 0..iterations) {
+            val maybeNotifications = conn.notifications
+            // If a notification arrived, stop fetching for it.
+            if (maybeNotifications.isNotEmpty()) {
+                // Double checking that the channel is correct.
+                // Notification(s) arrived, double-check channel name.
+                maybeNotifications.forEach {
+                    if (it.name != channel) {
+                        throw UtilError(
+                            statusCode = HttpStatusCode.InternalServerError,
+                            reason = "Listener got wrong notification.  
Expected: $channel, but got: ${it.name}"
+                        )
+                    }
+                }
+                notificationFound = true
+                break
+            }
+            /* Notification didn't arrive, release the thread and
+             * retry in the next chunk.  */
+            delay(sleepTimeMs)
+        }
+
+        if (!notificationFound) {
+            throw UtilError(
+                statusCode = HttpStatusCode.NotFound,
+                reason = "Timeout expired for notification on channel 
$channel",
+                ec = LibeufinErrorCode.LIBEUFIN_EC_TIMEOUT_EXPIRED
+            )
+        }
+        /* Notification arrived.  In this current version
+         * we don't pass any data to the caller; the channel
+         * name itself means that the awaited information arrived.
+         * */
+        return
+        }
+    }
\ No newline at end of file
diff --git a/util/src/main/kotlin/DBTypes.kt b/util/src/main/kotlin/DBTypes.kt
deleted file mode 100644
index c38e63cf..00000000
--- a/util/src/main/kotlin/DBTypes.kt
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * This file is part of LibEuFin.
- * Copyright (C) 2020 Taler Systems S.A.
- *
- * LibEuFin is free software; you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation; either version 3, or
- * (at your option) any later version.
- *
- * LibEuFin is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
- * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General
- * Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public
- * License along with LibEuFin; see the file COPYING.  If not, see
- * <http://www.gnu.org/licenses/>
- */
-
-package tech.libeufin.util
-
-import org.jetbrains.exposed.sql.Column
-import org.jetbrains.exposed.sql.ColumnType
-import org.jetbrains.exposed.sql.Table
-import java.math.BigDecimal
-import java.math.RoundingMode
-
-const val SCALE_TWO = 2
-const val NUMBER_MAX_DIGITS = 20
-class BadAmount(badValue: Any?) : Exception("Value '${badValue}' is not a 
valid amount")
-typealias Amount = BigDecimal
-
-class AmountColumnType : ColumnType() {
-    override fun sqlType(): String  = "DECIMAL(${NUMBER_MAX_DIGITS}, 
${SCALE_TWO})"
-        override fun valueFromDB(value: Any): Any {
-            val valueFromDB = super.valueFromDB(value)
-            try {
-                return when (valueFromDB) {
-                    is BigDecimal -> valueFromDB.setScale(SCALE_TWO, 
RoundingMode.UNNECESSARY)
-                    is Double -> 
BigDecimal.valueOf(valueFromDB).setScale(SCALE_TWO, RoundingMode.UNNECESSARY)
-                    is Float -> BigDecimal(valueFromDB.toString()).setScale(
-                        SCALE_TWO,
-                        RoundingMode.UNNECESSARY
-                    )
-                    is Int -> BigDecimal(valueFromDB)
-                    is Long -> BigDecimal.valueOf(valueFromDB)
-                    else -> valueFromDB
-                }
-            } catch (e: Exception) {
-                e.printStackTrace()
-                throw BadAmount(value)
-            }
-        }
-
-        override fun valueToDB(value: Any?): Any? {
-            try {
-                (value as BigDecimal).setScale(SCALE_TWO, 
RoundingMode.UNNECESSARY)
-            } catch (e: Exception) {
-                e.printStackTrace()
-                throw BadAmount(value)
-            }
-
-            if (value.compareTo(BigDecimal.ZERO) == 0) {
-                throw BadAmount(value)
-            }
-            return super.valueToDB(value)
-        }
-}
-
-/**
- * Make sure the number entered by upper layers does not need any rounding
- * to conform to scale == 2
- */
-fun Table.amount(name: String): Column<Amount> {
-    return registerColumn(name, AmountColumnType())
-}
\ No newline at end of file
diff --git a/util/src/main/kotlin/LibeufinErrorCodes.kt 
b/util/src/main/kotlin/LibeufinErrorCodes.kt
index 6020422a..758292e3 100644
--- a/util/src/main/kotlin/LibeufinErrorCodes.kt
+++ b/util/src/main/kotlin/LibeufinErrorCodes.kt
@@ -71,5 +71,7 @@ enum class LibeufinErrorCode(val code: Int) {
      * A request is using a unsupported currency.  Usually returned
      * along 400 Bad Request
      */
-    LIBEUFIN_EC_BAD_CURRENCY(7)
+    LIBEUFIN_EC_BAD_CURRENCY(7),
+
+    LIBEUFIN_EC_TIMEOUT_EXPIRED(8)
 }
\ No newline at end of file

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]