schleuder 2.2.4 → 3.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +138 -0
  3. data/Rakefile +136 -0
  4. data/bin/pinentry-clearpassphrase +72 -0
  5. data/bin/schleuder +9 -89
  6. data/bin/schleuder-api-daemon +4 -0
  7. data/db/migrate/20140501103532_create_lists.rb +39 -0
  8. data/db/migrate/20140501112859_create_subscriptions.rb +21 -0
  9. data/db/migrate/201508092100_add_language_to_lists.rb +11 -0
  10. data/db/migrate/20150812165700_change_keywords_admin_only_defaults.rb +8 -0
  11. data/db/migrate/20150813235800_add_forward_all_incoming_to_admins.rb +11 -0
  12. data/db/migrate/201508141727_change_send_encrypted_only_default.rb +8 -0
  13. data/db/migrate/201508222143_add_logfiles_to_keep_to_lists.rb +11 -0
  14. data/db/migrate/201508261723_rename_delivery_disabled_to_delivery_enabled_and_change_default.rb +14 -0
  15. data/db/migrate/201508261815_strip_gpg_passphrase.rb +11 -0
  16. data/db/migrate/201508261827_remove_default_mime.rb +9 -0
  17. data/db/migrate/20160501172700_fix_headers_to_meta_defaults.rb +8 -0
  18. data/db/migrate/20170713215059_add_internal_footer_to_list.rb +11 -0
  19. data/db/schema.rb +62 -0
  20. data/etc/init.d/schleuder-api-daemon +87 -0
  21. data/etc/list-defaults.yml +123 -0
  22. data/etc/postfix/schleuder_sqlite.cf +28 -0
  23. data/etc/schleuder-api-daemon.service +10 -0
  24. data/etc/schleuder.cron.weekly +6 -0
  25. data/etc/schleuder.yml +61 -0
  26. data/lib/schleuder-api-daemon.rb +420 -0
  27. data/lib/schleuder.rb +81 -47
  28. data/lib/schleuder/cli.rb +334 -0
  29. data/lib/schleuder/cli/cert.rb +24 -0
  30. data/lib/schleuder/cli/schleuder_cert_manager.rb +84 -0
  31. data/lib/schleuder/cli/subcommand_fix.rb +11 -0
  32. data/lib/schleuder/conf.rb +131 -0
  33. data/lib/schleuder/errors/active_model_error.rb +15 -0
  34. data/lib/schleuder/errors/base.rb +17 -0
  35. data/lib/schleuder/errors/decryption_failed.rb +16 -0
  36. data/lib/schleuder/errors/fatal_error.rb +13 -0
  37. data/lib/schleuder/errors/file_not_found.rb +14 -0
  38. data/lib/schleuder/errors/invalid_listname.rb +13 -0
  39. data/lib/schleuder/errors/key_adduid_failed.rb +13 -0
  40. data/lib/schleuder/errors/key_generation_failed.rb +16 -0
  41. data/lib/schleuder/errors/keyword_admin_only.rb +13 -0
  42. data/lib/schleuder/errors/list_exists.rb +13 -0
  43. data/lib/schleuder/errors/list_not_found.rb +14 -0
  44. data/lib/schleuder/errors/list_property_missing.rb +14 -0
  45. data/lib/schleuder/errors/listdir_problem.rb +16 -0
  46. data/lib/schleuder/errors/loading_list_settings_failed.rb +14 -0
  47. data/lib/schleuder/errors/message_empty.rb +14 -0
  48. data/lib/schleuder/errors/message_not_from_admin.rb +13 -0
  49. data/lib/schleuder/errors/message_sender_not_subscribed.rb +13 -0
  50. data/lib/schleuder/errors/message_too_big.rb +14 -0
  51. data/lib/schleuder/errors/message_unauthenticated.rb +13 -0
  52. data/lib/schleuder/errors/message_unencrypted.rb +13 -0
  53. data/lib/schleuder/errors/message_unsigned.rb +13 -0
  54. data/lib/schleuder/errors/standard_error.rb +5 -0
  55. data/lib/schleuder/errors/too_many_keys.rb +17 -0
  56. data/lib/schleuder/errors/unknown_list_option.rb +14 -0
  57. data/lib/schleuder/filters/auth_filter.rb +39 -0
  58. data/lib/schleuder/filters/bounces_filter.rb +12 -0
  59. data/lib/schleuder/filters/forward_filter.rb +17 -0
  60. data/lib/schleuder/filters/forward_incoming.rb +13 -0
  61. data/lib/schleuder/filters/hotmail_message_filter.rb +25 -0
  62. data/lib/schleuder/filters/max_message_size.rb +14 -0
  63. data/lib/schleuder/filters/request_filter.rb +26 -0
  64. data/lib/schleuder/filters/send_key_filter.rb +20 -0
  65. data/lib/schleuder/filters/strip_alternative_filter.rb +21 -0
  66. data/lib/schleuder/filters_runner.rb +83 -0
  67. data/lib/schleuder/gpgme/ctx.rb +274 -0
  68. data/lib/schleuder/gpgme/import_status.rb +27 -0
  69. data/lib/schleuder/gpgme/key.rb +212 -0
  70. data/lib/schleuder/gpgme/sub_key.rb +13 -0
  71. data/lib/schleuder/gpgme/user_id.rb +22 -0
  72. data/lib/schleuder/list.rb +318 -127
  73. data/lib/schleuder/list_builder.rb +139 -0
  74. data/lib/schleuder/listlogger.rb +31 -0
  75. data/lib/schleuder/logger.rb +23 -0
  76. data/lib/schleuder/logger_notifications.rb +69 -0
  77. data/lib/schleuder/mail/message.rb +482 -0
  78. data/lib/schleuder/mail/parts_list.rb +9 -0
  79. data/lib/schleuder/plugin_runners/base.rb +91 -0
  80. data/lib/schleuder/plugin_runners/list_plugins_runner.rb +24 -0
  81. data/lib/schleuder/plugin_runners/request_plugins_runner.rb +27 -0
  82. data/lib/schleuder/plugins/attach_listkey.rb +17 -0
  83. data/lib/schleuder/plugins/get_version.rb +7 -0
  84. data/lib/schleuder/plugins/key_management.rb +113 -0
  85. data/lib/schleuder/plugins/list_management.rb +15 -0
  86. data/lib/schleuder/plugins/resend.rb +196 -0
  87. data/lib/schleuder/plugins/sign_this.rb +46 -0
  88. data/lib/schleuder/plugins/subscription_management.rb +140 -0
  89. data/lib/schleuder/runner.rb +130 -0
  90. data/lib/schleuder/subscription.rb +98 -0
  91. data/lib/schleuder/validators/boolean_validator.rb +7 -0
  92. data/lib/schleuder/validators/email_validator.rb +7 -0
  93. data/lib/schleuder/validators/fingerprint_validator.rb +7 -0
  94. data/lib/schleuder/validators/greater_than_zero_validator.rb +7 -0
  95. data/lib/schleuder/validators/no_line_breaks_validator.rb +7 -0
  96. data/lib/schleuder/version.rb +1 -1
  97. data/locales/de.yml +179 -0
  98. data/locales/en.yml +179 -0
  99. metadata +305 -108
  100. checksums.yaml.gz.sig +0 -3
  101. data.tar.gz.sig +0 -2
  102. data/LICENSE +0 -339
  103. data/README +0 -32
  104. data/bin/schleuder-fix-gem-dependencies +0 -37
  105. data/bin/schleuder-init-setup +0 -37
  106. data/bin/schleuder-migrate-v2.1-to-v2.2 +0 -225
  107. data/bin/schleuder-newlist +0 -413
  108. data/contrib/check-expired-keys.rb +0 -60
  109. data/contrib/mutt-schleuder-colors.rc +0 -10
  110. data/contrib/mutt-schleuder-resend.vim +0 -24
  111. data/contrib/smtpserver.rb +0 -76
  112. data/ext/default-list.conf +0 -149
  113. data/ext/default-members.conf +0 -7
  114. data/ext/list.conf.example +0 -14
  115. data/ext/schleuder.conf +0 -64
  116. data/lib/schleuder/archiver.rb +0 -46
  117. data/lib/schleuder/crypt.rb +0 -210
  118. data/lib/schleuder/errors.rb +0 -5
  119. data/lib/schleuder/list_config.rb +0 -146
  120. data/lib/schleuder/log/listlogger.rb +0 -57
  121. data/lib/schleuder/log/outputter/emailoutputter.rb +0 -120
  122. data/lib/schleuder/log/outputter/metaemailoutputter.rb +0 -50
  123. data/lib/schleuder/log/schleuderlogger.rb +0 -34
  124. data/lib/schleuder/mail.rb +0 -873
  125. data/lib/schleuder/mailer.rb +0 -26
  126. data/lib/schleuder/member.rb +0 -69
  127. data/lib/schleuder/plugin.rb +0 -54
  128. data/lib/schleuder/processor.rb +0 -363
  129. data/lib/schleuder/schleuder_config.rb +0 -75
  130. data/lib/schleuder/storage.rb +0 -84
  131. data/lib/schleuder/utils.rb +0 -80
  132. data/man/schleuder-newlist.8 +0 -174
  133. data/man/schleuder.8 +0 -416
  134. data/plugins/README +0 -20
  135. data/plugins/manage_keys_plugin.rb +0 -113
  136. data/plugins/manage_members_plugin.rb +0 -156
  137. data/plugins/manage_self_plugin.rb +0 -26
  138. data/plugins/resend_plugin.rb +0 -35
  139. data/plugins/sign_this_plugin.rb +0 -14
  140. data/plugins/version_plugin.rb +0 -12
  141. metadata.gz.sig +0 -0
@@ -0,0 +1,46 @@
1
+ module Schleuder
2
+ module RequestPlugins
3
+ def self.sign_this(arguments, list, mail)
4
+ if mail.has_attachments?
5
+ list.logger.debug "Signing each attachment's body"
6
+ intro = I18n.t('plugins.signatures_attached')
7
+ parts = mail.attachments.map do |attachment|
8
+ make_signature_part(attachment, list)
9
+ end
10
+ [intro, parts].flatten
11
+ else
12
+ list.logger.debug "Clear-signing first available text/plain part"
13
+ clearsign(mail.first_plaintext_part)
14
+ end
15
+ end
16
+
17
+ # helper methods
18
+ private
19
+
20
+ def self.make_signature_part(attachment, list)
21
+ material = attachment.body.to_s
22
+ return nil if material.strip.blank?
23
+ file_basename = attachment.filename.presence || Digest::SHA256.hexdigest(material)
24
+ list.logger.debug "Signing #{file_basename}"
25
+ filename = "#{file_basename}.sig"
26
+ part = Mail::Part.new
27
+ part.body = detachsign(material)
28
+ part.content_type = 'application/pgp-signature'
29
+ part.content_disposition = "attachment; filename=#{filename}"
30
+ part.content_description = "OpenPGP signature for '#{file_basename}'"
31
+ part
32
+ end
33
+
34
+ def self.detachsign(thing)
35
+ crypto.sign(thing, mode: GPGME::SIG_MODE_DETACH).to_s
36
+ end
37
+
38
+ def self.clearsign(mail)
39
+ crypto.clearsign(mail.body.to_s).to_s
40
+ end
41
+
42
+ def self.crypto
43
+ @crypto ||= GPGME::Crypto.new(armor: true)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,140 @@
1
+ module Schleuder
2
+ module RequestPlugins
3
+ def self.subscribe(arguments, list, mail)
4
+ email = arguments.shift
5
+
6
+ if arguments.present?
7
+ # Collect all arguments that look like fingerprint-material
8
+ fingerprint = ''
9
+ while arguments.first.present? && arguments.first.match(/\A(0x)?[a-f0-9]+/i)
10
+ fingerprint << arguments.shift
11
+ end
12
+ # Use possibly remaining args as flags.
13
+ adminflag = arguments.shift
14
+ deliveryflag = arguments.shift
15
+ end
16
+
17
+ sub, _ = list.subscribe(email, fingerprint, adminflag, deliveryflag)
18
+
19
+ if sub.persisted?
20
+ I18n.t(
21
+ "plugins.subscription_management.subscribed",
22
+ email: sub.email,
23
+ fingerprint: sub.fingerprint,
24
+ admin: sub.admin,
25
+ delivery_enabled: sub.delivery_enabled
26
+ )
27
+ else
28
+ I18n.t(
29
+ "plugins.subscription_management.subscribing_failed",
30
+ email: sub.email,
31
+ errors: sub.errors.full_messages.join(".\n")
32
+ )
33
+ end
34
+ end
35
+
36
+ def self.unsubscribe(arguments, list, mail)
37
+ # If no address was given we unsubscribe the sender.
38
+ email = arguments.first.presence || mail.signer.email
39
+
40
+ # TODO: May signers have multiple UIDs? We don't match those currently.
41
+ if ! list.from_admin?(mail) && email != mail.signer.email
42
+ # Only admins may unsubscribe others.
43
+ return I18n.t(
44
+ "plugins.subscription_management.forbidden", email: email
45
+ )
46
+ end
47
+
48
+ sub = list.subscriptions.where(email: email).first
49
+
50
+ if sub.blank?
51
+ return I18n.t(
52
+ "plugins.subscription_management.is_not_subscribed", email: email
53
+ )
54
+ end
55
+
56
+ if res = sub.delete
57
+ I18n.t(
58
+ "plugins.subscription_management.unsubscribed", email: email
59
+ )
60
+ else
61
+ I18n.t(
62
+ "plugins.subscription_management.unsubscribing_failed",
63
+ email: email,
64
+ error: res.errors.to_a
65
+ )
66
+ end
67
+ end
68
+
69
+ def self.list_subscriptions(arguments, list, mail)
70
+ subs = if arguments.blank?
71
+ list.subscriptions.all.to_a
72
+ else
73
+ arguments.map do |argument|
74
+ list.subscriptions.where("email like ?", "%#{argument}%").to_a
75
+ end.flatten
76
+ end
77
+
78
+ if subs.blank?
79
+ return nil
80
+ end
81
+
82
+ out = [ I18n.t("plugins.subscription_management.list_of_subscriptions") ]
83
+
84
+ out << subs.map do |subscription|
85
+ # Fingerprints are at most 40 characters long, and lines shouldn't
86
+ # exceed 80 characters if possible.
87
+ s = subscription.email
88
+ if subscription.fingerprint.present?
89
+ s << "\t0x#{subscription.fingerprint}"
90
+ end
91
+ if ! subscription.delivery_enabled?
92
+ s << "\tDelivery disabled!"
93
+ end
94
+ s
95
+ end
96
+
97
+ out.join("\n")
98
+ end
99
+
100
+ def self.set_fingerprint(arguments, list, mail)
101
+ if arguments.first.match(/@/)
102
+ if arguments.first == mail.signer.email || list.from_admin?(mail)
103
+ email = arguments.shift
104
+ else
105
+ return I18n.t(
106
+ "plugins.subscription_management.set_fingerprint_only_self"
107
+ )
108
+ end
109
+ else
110
+ email = mail.signer.email
111
+ end
112
+
113
+ sub = list.subscriptions.where(email: email).first
114
+
115
+ if sub.blank?
116
+ return I18n.t(
117
+ "plugins.subscription_management.is_not_subscribed", email: email
118
+ )
119
+ end
120
+
121
+ sub.fingerprint = arguments.join
122
+
123
+ if sub.save
124
+ I18n.t(
125
+ "plugins.subscription_management.fingerprint_set",
126
+ email: email,
127
+ fingerprint: sub.fingerprint
128
+ )
129
+ else
130
+ I18n.t(
131
+ "plugins.subscription_management.setting_fingerprint_failed",
132
+ email: email,
133
+ fingerprint: arguments.last,
134
+ errors: sub.errors.to_a.join("\n")
135
+ )
136
+ end
137
+ end
138
+ end
139
+ end
140
+
@@ -0,0 +1,130 @@
1
+ module Schleuder
2
+ class Runner
3
+ def run(msg, recipient)
4
+ error = setup_list(recipient)
5
+ return error if error
6
+
7
+ logger.info "Parsing incoming email."
8
+ @mail = Mail.create_message_to_list(msg, recipient, list)
9
+
10
+ error = run_filters(Filters::Runner::PRE_SETUP_FILTERS)
11
+ return error if error
12
+
13
+ begin
14
+ # This decrypts, verifies, etc.
15
+ @mail = @mail.setup
16
+ rescue GPGME::Error::DecryptFailed
17
+ logger.warn "Decryption of incoming message failed."
18
+ return Errors::DecryptionFailed.new(list)
19
+ end
20
+
21
+ error = run_filters(Filters::Runner::POST_SETUP_FILTERS)
22
+ return error if error
23
+
24
+ if ! @mail.was_validly_signed?
25
+ logger.debug "Message was not validly signed, adding subject_prefix_in"
26
+ @mail.add_subject_prefix_in!
27
+ end
28
+
29
+ if ! @mail.was_encrypted?
30
+ logger.debug "Message was not encrypted, skipping plugins"
31
+ elsif @mail.was_validly_signed?
32
+ # Plugins
33
+ logger.debug "Message was encrypted and validly signed"
34
+ PluginRunners::ListPluginsRunner.run(list, @mail).compact
35
+ end
36
+
37
+ # Don't send empty messages over the list.
38
+ if @mail.empty?
39
+ logger.info "Message found empty, not sending it to list."
40
+ return Errors::MessageEmpty.new(@list)
41
+ end
42
+
43
+ logger.debug "Adding subject_prefix"
44
+ @mail.add_subject_prefix!
45
+
46
+ # Subscriptions
47
+ logger.debug "Creating clean copy of message"
48
+ copy = @mail.clean_copy(true)
49
+ list.send_to_subscriptions(copy)
50
+ nil
51
+ end
52
+
53
+ private
54
+
55
+ def list
56
+ @list
57
+ end
58
+
59
+ def run_filters(filters)
60
+ error = filters_runner.run(@mail, filters)
61
+ if error
62
+ if list.bounces_notify_admins?
63
+ text = "#{I18n.t('.bounces_notify_admins')}\n\n#{error}"
64
+ # TODO: raw_source is mostly blank?
65
+ logger.notify_admin text, @mail.original_message, I18n.t('notice')
66
+ end
67
+ return error
68
+ end
69
+ end
70
+
71
+ def filters_runner
72
+ @filters_runner ||= Filters::Runner.new(list)
73
+ end
74
+
75
+ def logger
76
+ list.present? && list.logger || Schleuder.logger
77
+ end
78
+
79
+ def log_and_return(error, reveal_error=false)
80
+ Schleuder.logger.error(error)
81
+ if reveal_error
82
+ error
83
+ else
84
+ # Return an unrevealing error, the sender and all bystanders don't need to know these details.
85
+ Errors::FatalError.new
86
+ end
87
+ end
88
+
89
+ def setup_list(recipient)
90
+ return @list if @list
91
+
92
+ logger.info "Loading list '#{recipient}'"
93
+ if ! @list = List.by_recipient(recipient)
94
+ return log_and_return(Errors::ListNotFound.new(recipient), true)
95
+ end
96
+
97
+ # Check neccessary permissions of crucial files.
98
+ if ! File.exist?(@list.listdir)
99
+ return log_and_return(Errors::ListdirProblem.new(@list.listdir, :not_existing))
100
+ elsif ! File.directory?(@list.listdir)
101
+ return log_and_return(Errors::ListdirProblem.new(@list.listdir, :not_a_directory))
102
+ elsif ! File.readable?(@list.listdir)
103
+ return log_and_return(Errors::ListdirProblem.new(@list.listdir, :not_readable))
104
+ elsif ! File.writable?(@list.listdir)
105
+ return log_and_return(Errors::ListdirProblem.new(@list.listdir, :not_writable))
106
+ else
107
+ if File.exist?(@list.logfile) && ! File.writable?(@list.logfile)
108
+ return log_and_return(Errors::ListdirProblem.new(@list.logfile, :not_writable))
109
+ end
110
+ end
111
+
112
+ # Check basic sanity of list.
113
+ %w[fingerprint key secret_key admins].each do |attrib|
114
+ if @list.send(attrib).blank?
115
+ return log_and_return(Errors::ListPropertyMissing.new(@list.listdir, attrib))
116
+ end
117
+ end
118
+
119
+ # Set locale
120
+ if I18n.available_locales.include?(@list.language.to_sym)
121
+ I18n.locale = @list.language.to_sym
122
+ end
123
+
124
+ # This cannot be put in List, as Mail wouldn't know it then.
125
+ logger.debug "Setting GNUPGHOME to #{@list.listdir}"
126
+ ENV['GNUPGHOME'] = @list.listdir
127
+ nil
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,98 @@
1
+ module Schleuder
2
+ class Subscription < ActiveRecord::Base
3
+ belongs_to :list
4
+
5
+ validates :list_id, inclusion: {
6
+ in: -> (id) { List.pluck(:id) },
7
+ message: "must refer to an existing list"
8
+ }
9
+ validates :email, presence: true, email: true, uniqueness: {scope: :list_id}
10
+ validates :fingerprint, allow_blank: true, fingerprint: true
11
+ validates :delivery_enabled, :admin, boolean: true
12
+
13
+ default_scope { order(:email) }
14
+
15
+ scope :without_fingerprint, -> { where(fingerprint: [nil,'']) }
16
+
17
+ def to_s
18
+ email
19
+ end
20
+
21
+ def self.configurable_attributes
22
+ [:fingerprint, :admin, :delivery_enabled]
23
+ end
24
+
25
+ def fingerprint=(arg)
26
+ # Allow input to contain whitespace and '0x'-prefix, but don't store it
27
+ # into the DB.
28
+ value = arg.to_s.gsub(/\s*/, '').gsub(/^0x/, '').chomp
29
+ write_attribute(:fingerprint, value)
30
+ end
31
+
32
+ def key
33
+ # TODO: make key-related methods a concern, so we don't have to go
34
+ # through the list and neither re-implement the methods here.
35
+ # Prefix '0x' to force GnuPG to match only hex-values, not UIDs.
36
+ list.keys("0x#{self.fingerprint}").first
37
+ end
38
+
39
+ def send_mail(mail)
40
+ list.logger.debug "Preparing sending to #{self.inspect}"
41
+
42
+ if ! self.delivery_enabled
43
+ list.logger.info "Not sending to #{self.email}: delivery is disabled."
44
+ return false
45
+ end
46
+
47
+ mail = ensure_headers(mail)
48
+ gpg_opts = self.list.gpg_sign_options
49
+
50
+ if self.key.blank?
51
+ if self.list.send_encrypted_only?
52
+ notify_of_missed_message(:absent)
53
+ return false
54
+ else
55
+ list.logger.warn "Sending plaintext because no key is present!"
56
+ end
57
+ elsif ! self.key.usable?
58
+ if self.list.send_encrypted_only?
59
+ notify_of_missed_message(key.usability_issue)
60
+ return false
61
+ else
62
+ list.logger.warn "Sending plaintext because assigned key is #{key.usability_issue}!"
63
+ end
64
+ else
65
+ gpg_opts.merge!(encrypt: true, keys: {self.email => "0x#{self.fingerprint}"})
66
+ end
67
+
68
+ list.logger.info "Sending message to #{self.email}"
69
+ mail.gpg gpg_opts
70
+ mail.deliver
71
+ end
72
+
73
+ def ensure_headers(mail)
74
+ mail.to = self.email
75
+ mail.from = self.list.email
76
+ mail.sender = self.list.bounce_address
77
+ mail
78
+ end
79
+
80
+ def notify_of_missed_message(reason)
81
+ self.list.logger.warn "Not sending to #{self.email}: key is unusable because it is #{reason} and sending plain text not allowed"
82
+ mail = ensure_headers(Mail.new)
83
+ mail.subject = I18n.t('notice')
84
+ mail.body = I18n.t("missed_message_due_to_unusable_key", list_email: self.list.email) + I18n.t('errors.signoff')
85
+ mail.gpg self.list.gpg_sign_options
86
+ mail.deliver
87
+ end
88
+
89
+ def admin?
90
+ self.admin == true
91
+ end
92
+
93
+ def delete_key
94
+ list.delete_key(self.fingerprint)
95
+ end
96
+
97
+ end
98
+ end
@@ -0,0 +1,7 @@
1
+ class BooleanValidator < ActiveModel::EachValidator
2
+ def validate_each(record, attribute, value)
3
+ if ! [true, false].include?(value)
4
+ record.errors.add(attribute, I18n.t("errors.must_be_boolean"))
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ class EmailValidator < ActiveModel::EachValidator
2
+ def validate_each(record, attribute, value)
3
+ unless value =~ Conf::EMAIL_REGEXP
4
+ record.errors[attribute] << (options[:message] || I18n.t("errors.invalid_email"))
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ class FingerprintValidator < ActiveModel::EachValidator
2
+ def validate_each(record, attribute, value)
3
+ unless value =~ /\A[a-f0-9]{32,}\z/i
4
+ record.errors[attribute] << (options[:message] || I18n.t("errors.invalid_fingerprint"))
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ class GreaterThanZeroValidator < ActiveModel::EachValidator
2
+ def validate_each(record, attribute, value)
3
+ if value.to_i == 0
4
+ record.errors.add(attribute, I18n.t("errors.must_be_greater_than_zero"))
5
+ end
6
+ end
7
+ end